<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>File-Permissions on Yarang's Tech Lair</title><link>https://blog.fcoinfup.com/ko/tags/file-permissions/</link><description>Recent content in File-Permissions on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Sun, 03 May 2026 01:10:00 +0900</lastBuildDate><atom:link href="https://blog.fcoinfup.com/ko/tags/file-permissions/index.xml" rel="self" type="application/rss+xml"/><item><title>블로그 AI 자동 댓글 시스템 구축기 (2/3) — 보안 강화</title><link>https://blog.fcoinfup.com/ko/post/ai-auto-comment-system-part2-security/</link><pubDate>Sun, 03 May 2026 01:10:00 +0900</pubDate><guid>https://blog.fcoinfup.com/ko/post/ai-auto-comment-system-part2-security/</guid><description>&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://blog.fcoinfup.com/ko/post/ai-auto-comment-system-part1-architecture/" &gt;1부&lt;/a&gt;에서 AI 자동 댓글 시스템의 아키텍처와 구현을 다뤘습니다. 이번 2부에서는 보안 측면을 집중적으로 다룹니다.&lt;/p&gt;
&lt;p&gt;외부 Webhook을 수신하고, GitHub API 토큰을 관리하며, 사용자 입력을 처리하는 시스템은 보안이 특히 중요합니다. 환경 변수 대신 파일 기반 인증으로 전환한 과정과 그 이유, 각 보안 계층의 설계를 설명합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="보안-위협-모델"&gt;보안 위협 모델
&lt;/h2&gt;&lt;p&gt;이 시스템이 방어해야 하는 위협:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;위협&lt;/th&gt;
 &lt;th&gt;공격 벡터&lt;/th&gt;
 &lt;th&gt;방어 수단&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;위장 Webhook&lt;/td&gt;
 &lt;td&gt;공격자가 가짜 Webhook 전송&lt;/td&gt;
 &lt;td&gt;HMAC-SHA256 시그니처 검증&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;토큰 유출&lt;/td&gt;
 &lt;td&gt;환경 변수 노출, 로그 노출&lt;/td&gt;
 &lt;td&gt;파일 기반 인증 + 권한 제한&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;XSS/인젝션&lt;/td&gt;
 &lt;td&gt;악의적인 댓글 내용&lt;/td&gt;
 &lt;td&gt;입력 sanitization&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;과도한 요청&lt;/td&gt;
 &lt;td&gt;DDoS, 남용&lt;/td&gt;
 &lt;td&gt;Flask-Limiter 속도 제한&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;권한 상승&lt;/td&gt;
 &lt;td&gt;워커 프로세스 탈취&lt;/td&gt;
 &lt;td&gt;systemd 보안 디렉티브&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;무한 루프&lt;/td&gt;
 &lt;td&gt;AI가 자신에게 응답&lt;/td&gt;
 &lt;td&gt;마커 기반 댓글 감지&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="파일-기반-인증-관리"&gt;파일 기반 인증 관리
&lt;/h2&gt;&lt;h3 id="환경-변수의-문제점"&gt;환경 변수의 문제점
&lt;/h3&gt;&lt;p&gt;처음에는 GitHub 토큰과 Webhook 시크릿을 환경 변수로 관리했습니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 초기 (안전하지 않음)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;GITHUB_TOKEN=ghp_xxxxx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;GITHUB_WEBHOOK_SECRET=my-secret-key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;환경 변수 방식의 문제점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/proc/PID/environ&lt;/code&gt;&lt;/strong&gt;: Linux에서 프로세스의 환경 변수가 파일로 노출됨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로그 노출&lt;/strong&gt;: 디버깅 중 환경 변수가 로그에 기록될 위험&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자식 프로세스 상속&lt;/strong&gt;: &lt;code&gt;subprocess.run&lt;/code&gt;으로 Claude Code 실행 시 모든 환경 변수가 상속됨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd 설정 파일&lt;/strong&gt;: 서비스 파일에 평문 시크릿이 포함되면 git에 커밋될 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="파일-기반으로-전환"&gt;파일 기반으로 전환
&lt;/h3&gt;&lt;p&gt;인증 정보를 파일 시스템에 저장하고, 환경 변수에는 &lt;strong&gt;파일 경로만&lt;/strong&gt; 지정합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 개선 후 — 경로만 노출&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;GITHUB_TOKEN_FILE=/etc/auto-comment-worker/github-token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;GITHUB_WEBHOOK_SECRET_FILE=/etc/auto-comment-worker/credentials/webhook-secret&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;서버의 디렉토리 구조:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/etc/auto-comment-worker/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── github-token # GitHub Personal Access Token (640)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── credentials/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── webhook-secret # GitHub Webhook HMAC Secret (600)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="토큰-파일-로드-코드"&gt;토큰 파일 로드 코드
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; stat
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;GITHUB_TOKEN_FILE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;GITHUB_TOKEN_FILE&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; GITHUB_TOKEN_FILE &lt;span style="color:#f92672"&gt;and&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;path&lt;span style="color:#f92672"&gt;.&lt;/span&gt;exists(GITHUB_TOKEN_FILE):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 파일 권한 검증&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; st &lt;span style="color:#f92672"&gt;=&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stat(GITHUB_TOKEN_FILE)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; st&lt;span style="color:#f92672"&gt;.&lt;/span&gt;st_mode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; stat&lt;span style="color:#f92672"&gt;.&lt;/span&gt;S_IWOTH:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Token file must not be world-writable&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(GITHUB_TOKEN_FILE, &lt;span style="color:#e6db74"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; GITHUB_TOKEN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; f&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; GITHUB_TOKEN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;GITHUB_TOKEN&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;핵심 설계 결정:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;파일 존재 시 파일 우선&lt;/strong&gt;: 환경 변수는 폴백으로만 사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;권한 검증&lt;/strong&gt;: 파일을 읽기 전에 권한을 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strip()&lt;/strong&gt;: 파일 끝의 개행 문자 제거&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="파일-권한-검증--삽질의-기록"&gt;파일 권한 검증 — 삽질의 기록
&lt;/h3&gt;&lt;p&gt;이 부분에서 가장 많은 시간을 소비했습니다. 초기 코드는 지나치게 엄격했습니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 초기 코드 — 너무 엄격함&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; st&lt;span style="color:#f92672"&gt;.&lt;/span&gt;st_mode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; (stat&lt;span style="color:#f92672"&gt;.&lt;/span&gt;S_IRWXO &lt;span style="color:#f92672"&gt;|&lt;/span&gt; stat&lt;span style="color:#f92672"&gt;.&lt;/span&gt;S_IRWXG):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Token file must be 600 or 400&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 코드의 문제: &lt;code&gt;S_IRWXO | S_IRWXG&lt;/code&gt;는 &lt;strong&gt;그룹의 모든 권한&lt;/strong&gt;(읽기/쓰기/실행)과 &lt;strong&gt;기타의 모든 권한&lt;/strong&gt;을 검사합니다. 즉, 파일 권한이 &lt;code&gt;640&lt;/code&gt; (소유자 읽기/쓰기, 그룹 읽기)이면 거부됩니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# 비트 마스크 분석
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;S_IRWXG = 0o070 # 그룹의 읽기+쓰기+실행
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;S_IRWXO = 0o007 # 기타의 읽기+쓰기+실행
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# 640 = 0o640
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;0o640 &amp;amp; (0o070 | 0o007) = 0o640 &amp;amp; 0o077 = 0o040 # 0이 아님 → 거부!
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;실제 보안상 중요한 것은 &lt;strong&gt;다른 사용자(other)가 파일을 수정할 수 없는 것&lt;/strong&gt;입니다. 그룹 읽기 권한은 같은 그룹 사용자가 파일을 읽을 수 있게 해주며, 보안상 문제가 되지 않습니다.&lt;/p&gt;
&lt;p&gt;수정 후:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 수정 후 — 실제 위협에 집중&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; st&lt;span style="color:#f92672"&gt;.&lt;/span&gt;st_mode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; stat&lt;span style="color:#f92672"&gt;.&lt;/span&gt;S_IWOTH:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Token file must not be world-writable&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;stat.S_IWOTH&lt;/code&gt; (&lt;code&gt;0o002&lt;/code&gt;)만 검사하면 됩니다. 이는 &amp;ldquo;기타 사용자에게 쓰기 권한이 있는가&amp;quot;만 확인합니다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;권한&lt;/th&gt;
 &lt;th&gt;8진수&lt;/th&gt;
 &lt;th&gt;초기 코드&lt;/th&gt;
 &lt;th&gt;수정 후&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;600&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0o600&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;허용&lt;/td&gt;
 &lt;td&gt;허용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;640&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0o640&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;거부&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;허용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;644&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0o644&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;거부&lt;/td&gt;
 &lt;td&gt;허용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;646&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0o646&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;거부&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;거부&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;666&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0o666&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;거부&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;거부&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="hmac-sha256-시그니처-검증"&gt;HMAC-SHA256 시그니처 검증
&lt;/h2&gt;&lt;p&gt;GitHub Webhook은 요청 본문을 Webhook 시크릿으로 HMAC-SHA256 해싱한 시그니처를 &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; 헤더로 전송합니다. 이를 검증하여 요청이 실제 GitHub에서 왔는지 확인합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;verify_webhook_signature&lt;/span&gt;(payload: bytes, signature: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;GitHub 웹훅 시그니처 검증&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; signature:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; logger&lt;span style="color:#f92672"&gt;.&lt;/span&gt;warning(&lt;span style="color:#e6db74"&gt;&amp;#34;Missing webhook signature&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; WEBHOOK_SECRET:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; logger&lt;span style="color:#f92672"&gt;.&lt;/span&gt;warning(&lt;span style="color:#e6db74"&gt;&amp;#34;WEBHOOK_SECRET not configured - skipping validation&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 개발 모드 허용&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hash_algorithm, github_signature &lt;span style="color:#f92672"&gt;=&lt;/span&gt; signature&lt;span style="color:#f92672"&gt;.&lt;/span&gt;split(&lt;span style="color:#e6db74"&gt;&amp;#39;=&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; hash_algorithm &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;sha256&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mac &lt;span style="color:#f92672"&gt;=&lt;/span&gt; hmac&lt;span style="color:#f92672"&gt;.&lt;/span&gt;new(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; WEBHOOK_SECRET&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;payload,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; digestmod&lt;span style="color:#f92672"&gt;=&lt;/span&gt;hashlib&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sha256
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expected_signature &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mac&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hexdigest()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 타이밍 공격 방지&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; hmac&lt;span style="color:#f92672"&gt;.&lt;/span&gt;compare_digest(expected_signature, github_signature):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Exception&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; logger&lt;span style="color:#f92672"&gt;.&lt;/span&gt;error(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Signature verification error: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;e&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;주요 포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hmac.compare_digest()&lt;/code&gt;&lt;/strong&gt;: 일반 &lt;code&gt;==&lt;/code&gt; 비교 대신 상수 시간 비교를 사용하여 타이밍 공격을 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;raw bytes 사용&lt;/strong&gt;: &lt;code&gt;request.data&lt;/code&gt; (원본 바이트)를 사용합니다. &lt;code&gt;request.json&lt;/code&gt;으로 파싱 후 재직렬화하면 원본과 달라질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개발 모드&lt;/strong&gt;: 시크릿이 설정되지 않으면 검증을 건너뜁니다. 프로덕션에서는 반드시 시크릿을 설정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-헤더-포워딩"&gt;nginx 헤더 포워딩
&lt;/h3&gt;&lt;p&gt;시그니처 검증이 정상 작동하려면 nginx가 &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; 헤더를 포워딩해야 합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/webhook&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http://localhost:8081&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; $host;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Real-IP&lt;/span&gt; $remote_addr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Hub-Signature-256&lt;/span&gt; $http_x_hub_signature_256;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;X-Hub-Signature-256&lt;/code&gt;을 명시적으로 포워딩하지 않으면, 기본 &lt;code&gt;proxy_pass&lt;/code&gt;만으로는 커스텀 헤더가 전달되지 않을 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="요청-검증-marshmallow-스키마"&gt;요청 검증 (marshmallow 스키마)
&lt;/h2&gt;&lt;p&gt;Webhook 페이로드의 구조를 marshmallow 스키마로 검증합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;WebhookSchema&lt;/span&gt;(Schema):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; action &lt;span style="color:#f92672"&gt;=&lt;/span&gt; fields&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Str(required&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, validate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;validate&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Equal(&lt;span style="color:#e6db74"&gt;&amp;#39;created&amp;#39;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; comment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; fields&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dict(required&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; discussion &lt;span style="color:#f92672"&gt;=&lt;/span&gt; fields&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dict(required&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repository &lt;span style="color:#f92672"&gt;=&lt;/span&gt; fields&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dict(required&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sender &lt;span style="color:#f92672"&gt;=&lt;/span&gt; fields&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dict(required&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;action = 'created'&lt;/code&gt;만 허용&lt;/strong&gt;: 댓글 수정(&lt;code&gt;edited&lt;/code&gt;)이나 삭제(&lt;code&gt;deleted&lt;/code&gt;) 이벤트는 거부합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필수 필드 검증&lt;/strong&gt;: &lt;code&gt;comment&lt;/code&gt;, &lt;code&gt;discussion&lt;/code&gt;, &lt;code&gt;repository&lt;/code&gt;가 없으면 400 에러를 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ValidationError → 감사 로그&lt;/strong&gt;: 잘못된 요청은 감사 로그에 기록됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="입력-sanitization"&gt;입력 Sanitization
&lt;/h2&gt;&lt;p&gt;사용자 댓글은 외부 입력이므로 반드시 처리합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitize_comment&lt;/span&gt;(body: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;사용자 입력 sanitization&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; re&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(&lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;[^&amp;gt;]+&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;, body) &lt;span style="color:#75715e"&gt;# HTML 태그 제거&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; html&lt;span style="color:#f92672"&gt;.&lt;/span&gt;escape(body) &lt;span style="color:#75715e"&gt;# 특수 문자 이스케이프&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; body[:&lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt;] &lt;span style="color:#75715e"&gt;# 길이 제한&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 sanitization이 적용되는 곳:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;댓글 본문 (&lt;code&gt;comment_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;토론 제목과 본문 (&lt;code&gt;discussion_title&lt;/code&gt;, &lt;code&gt;discussion_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;원작성자 이름 (&lt;code&gt;original_author&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;AI 응답 게시 시 인용 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="사용자명-마스킹"&gt;사용자명 마스킹
&lt;/h3&gt;&lt;p&gt;로그에 사용자 이름 전체를 기록하지 않습니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mask_username&lt;/span&gt;(username: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;사용자명 마스킹&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; username &lt;span style="color:#f92672"&gt;or&lt;/span&gt; len(username) &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;***&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;username[:&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;***&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;로그에는 &lt;code&gt;yar***&lt;/code&gt;처럼 마스킹된 이름만 표시됩니다. 개인정보 보호와 디버깅 편의성 사이의 균형을 맞춥니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="속도-제한"&gt;속도 제한
&lt;/h2&gt;&lt;p&gt;Flask-Limiter로 엔드포인트별 속도 제한을 적용합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;limiter &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Limiter(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app&lt;span style="color:#f92672"&gt;=&lt;/span&gt;app,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; key_func&lt;span style="color:#f92672"&gt;=&lt;/span&gt;get_remote_address,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; default_limits&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;10 per minute&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; storage_uri&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;memory://&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.route&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/webhook&amp;#39;&lt;/span&gt;, methods&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@limiter.limit&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;10 per minute&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;github_webhook&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;분당 10회 제한&lt;/strong&gt;: 정상적인 Webhook 호출 빈도를 고려한 수치&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;get_remote_address&lt;/code&gt;&lt;/strong&gt;: 클라이언트 IP 기준으로 제한&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;memory://&lt;/code&gt;&lt;/strong&gt;: 인메모리 저장소 (단일 프로세스에 적합)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="systemd-보안-디렉티브"&gt;systemd 보안 디렉티브
&lt;/h2&gt;&lt;p&gt;3부에서 systemd 배포를 상세히 다루지만, 보안 관련 디렉티브는 여기서 설명합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 보안 강화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;NoNewPrivileges&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true # 권한 상승 방지&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PrivateTmp&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true # 독립된 /tmp 제공&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ProtectSystem&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;strict # 파일시스템 읽기 전용&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ProtectHome&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;false # 홈 디렉토리 접근 허용 (Claude Code 설정)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ReadWritePaths&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/var/www/auto-comment-worker /var/log/auto-comment-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 리소스 제한&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MemoryMax&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;512M # 메모리 제한&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;CPUQuota&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;50% # CPU 사용량 제한&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;TasksMax&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;100 # 프로세스 수 제한&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;디렉티브&lt;/th&gt;
 &lt;th&gt;효과&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;NoNewPrivileges=true&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;setuid&lt;/code&gt;, &lt;code&gt;setgid&lt;/code&gt; 등으로 권한 상승 불가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PrivateTmp=true&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;독립된 &lt;code&gt;/tmp&lt;/code&gt; 네임스페이스, 다른 프로세스와 격리&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;전체 파일시스템을 읽기 전용으로 마운트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ReadWritePaths&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;명시적으로 쓰기를 허용할 경로만 지정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;MemoryMax=512M&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;OOM 상황에서 시스템 전체를 보호&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="protectsystemstrict과-readonlypaths의-충돌"&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt;과 &lt;code&gt;ReadOnlyPaths&lt;/code&gt;의 충돌
&lt;/h3&gt;&lt;p&gt;초기에 &lt;code&gt;ReadOnlyPaths=/etc/auto-comment-worker&lt;/code&gt;를 추가했다가 토큰 파일을 읽지 못하는 문제가 발생했습니다. &lt;code&gt;ProtectSystem=strict&lt;/code&gt;가 이미 전체 파일시스템을 읽기 전용으로 설정하므로, 별도의 &lt;code&gt;ReadOnlyPaths&lt;/code&gt;는 불필요합니다. 오히려 일부 환경에서 충돌을 일으킬 수 있어 제거했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마무리"&gt;마무리
&lt;/h2&gt;&lt;p&gt;이번 2부에서는 파일 기반 인증 관리, 파일 권한 검증 삽질기, HMAC-SHA256 시그니처 검증, 입력 sanitization, 속도 제한, systemd 보안 디렉티브를 다뤘습니다.&lt;/p&gt;
&lt;p&gt;보안에서 가장 중요한 교훈: &lt;strong&gt;&amp;ldquo;너무 엄격한 검증은 너무 느슨한 검증만큼 해롭다.&amp;rdquo;&lt;/strong&gt; 파일 권한을 &lt;code&gt;600&lt;/code&gt;만 허용하는 초기 코드는 보안상 안전했지만, 실제 운영 환경에서 &lt;code&gt;640&lt;/code&gt; 권한의 파일을 거부하여 서비스가 시작되지 않았습니다. 실제 위협(world-writable)에만 집중하는 것이 올바른 접근입니다.&lt;/p&gt;
&lt;p&gt;다음 3부에서는 &lt;strong&gt;배포와 트러블슈팅&lt;/strong&gt; — systemd 서비스 구성, nginx 리버스 프록시, 실제 겪은 오류들과 해결 과정을 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;이 글은 AgentForge 블로그 자동 댓글 시스템 시리즈의 2부입니다.&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>