<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hardening on Yarang's Tech Lair</title><link>https://blog.fcoinfup.com/tags/hardening/</link><description>Recent content in Hardening on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 03 May 2026 01:10:00 +0900</lastBuildDate><atom:link href="https://blog.fcoinfup.com/tags/hardening/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Blog AI Auto-Comment System (2/3) — Security Hardening</title><link>https://blog.fcoinfup.com/post/ai-auto-comment-system-part2-security/</link><pubDate>Sun, 03 May 2026 01:10:00 +0900</pubDate><guid>https://blog.fcoinfup.com/post/ai-auto-comment-system-part2-security/</guid><description>&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;In &lt;a class="link" href="https://blog.fcoinfup.com/ko/post/ai-auto-comment-system-part1-architecture/" &gt;Part 1&lt;/a&gt;, we covered the architecture and implementation of the AI auto-comment system. In this Part 2, we will focus on security aspects.&lt;/p&gt;
&lt;p&gt;Systems that receive external Webhooks, manage GitHub API tokens, and process user input require special attention to security. We will explain the process of switching from environment variables to file-based authentication, the reasons behind it, and the design of each security layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="security-threat-model"&gt;Security Threat Model
&lt;/h2&gt;&lt;p&gt;Threats that this system must defend against:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Threat&lt;/th&gt;
 &lt;th&gt;Attack Vector&lt;/th&gt;
 &lt;th&gt;Defense&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Spoofed Webhook&lt;/td&gt;
 &lt;td&gt;Attacker sends fake Webhook&lt;/td&gt;
 &lt;td&gt;HMAC-SHA256 signature verification&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Token Leak&lt;/td&gt;
 &lt;td&gt;Environment variable exposure, log exposure&lt;/td&gt;
 &lt;td&gt;File-based authentication + permission restrictions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;XSS/Injection&lt;/td&gt;
 &lt;td&gt;Malicious comment content&lt;/td&gt;
 &lt;td&gt;Input sanitization&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Excessive Requests&lt;/td&gt;
 &lt;td&gt;DDoS, abuse&lt;/td&gt;
 &lt;td&gt;Flask-Limiter rate limiting&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Privilege Escalation&lt;/td&gt;
 &lt;td&gt;Worker process compromise&lt;/td&gt;
 &lt;td&gt;systemd security directives&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Infinite Loop&lt;/td&gt;
 &lt;td&gt;AI responding to itself&lt;/td&gt;
 &lt;td&gt;Marker-based comment detection&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="file-based-authentication-management"&gt;File-Based Authentication Management
&lt;/h2&gt;&lt;h3 id="problems-with-environment-variables"&gt;Problems with Environment Variables
&lt;/h3&gt;&lt;p&gt;Initially, we managed GitHub tokens and Webhook secrets as environment variables:&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;# Initial (unsafe)&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;Problems with the environment variable approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/proc/PID/environ&lt;/code&gt;&lt;/strong&gt;: Process environment variables are exposed as a file in Linux&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log exposure&lt;/strong&gt;: Risk of environment variables being logged during debugging&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Child process inheritance&lt;/strong&gt;: When running Claude Code with &lt;code&gt;subprocess.run&lt;/code&gt;, all environment variables are inherited&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd configuration file&lt;/strong&gt;: If the service file contains plaintext secrets, there is a risk of committing them to git&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="switching-to-file-based"&gt;Switching to File-Based
&lt;/h3&gt;&lt;p&gt;Store credentials in the file system and specify only &lt;strong&gt;file paths&lt;/strong&gt; in environment variables:&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;# Improved — only path exposed&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;Directory structure of the server:&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="token-file-loading-code"&gt;Token File Loading Code
&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;# Check file permissions&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;Core design decisions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prefer file if it exists&lt;/strong&gt;: Environment variables are used only as a fallback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission verification&lt;/strong&gt;: Check permissions before reading the file&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strip()&lt;/strong&gt;: Remove newline characters at the end of the file&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="file-permission-validation--a-trial-and-error-journey"&gt;File Permission Validation — A Trial and Error Journey
&lt;/h3&gt;&lt;p&gt;I spent the most time on this part. The initial code was too strict:&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;# Initial code — too strict&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;Problem with this code: &lt;code&gt;S_IRWXO | S_IRWXG&lt;/code&gt; checks &lt;strong&gt;all group permissions&lt;/strong&gt; (read/write/execute) and &lt;strong&gt;all other permissions&lt;/strong&gt;. That is, if the file permission is &lt;code&gt;640&lt;/code&gt; (owner read/write, group read), it is rejected.&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;# Bit mask analysis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;S_IRWXG = 0o070 # Group read+write+execute
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;S_IRWXO = 0o007 # Other read+write+execute
&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 # Not 0 → Rejected!
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For actual security, what matters is that &lt;strong&gt;other users cannot modify the file&lt;/strong&gt;. Group read permission allows users in the same group to read the file and is not a security issue.&lt;/p&gt;
&lt;p&gt;After revision:&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;# After revision — focus on actual threat&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;You only need to check &lt;code&gt;stat.S_IWOTH&lt;/code&gt; (&lt;code&gt;0o002&lt;/code&gt;). This confirms only &amp;ldquo;Does it have write permission for others?&amp;rdquo;.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Permission&lt;/th&gt;
 &lt;th&gt;Octal&lt;/th&gt;
 &lt;th&gt;Initial Code&lt;/th&gt;
 &lt;th&gt;After Revision&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;Allowed&lt;/td&gt;
 &lt;td&gt;Allowed&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;Rejected&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Allowed&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;Rejected&lt;/td&gt;
 &lt;td&gt;Allowed&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;Rejected&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Rejected&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;Rejected&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="hmac-sha256-signature-verification"&gt;HMAC-SHA256 Signature Verification
&lt;/h2&gt;&lt;p&gt;GitHub Webhook sends a signature created by HMAC-SHA256 hashing the request body with the Webhook secret in the &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; header. We verify this to confirm that the request actually came from 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 webhook signature verification&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;# Allow for development mode&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;# Prevent timing attacks&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;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hmac.compare_digest()&lt;/code&gt;&lt;/strong&gt;: Uses constant-time comparison instead of normal &lt;code&gt;==&lt;/code&gt; comparison to prevent timing attacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;raw bytes usage&lt;/strong&gt;: Uses &lt;code&gt;request.data&lt;/code&gt; (original bytes). If parsed with &lt;code&gt;request.json&lt;/code&gt; and re-serialized, it may differ from the original.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development mode&lt;/strong&gt;: Skips validation if the secret is not set. The secret must be set in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-header-forwarding"&gt;nginx Header Forwarding
&lt;/h3&gt;&lt;p&gt;For signature verification to work properly, nginx must forward the &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; header:&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;Unless &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; is explicitly forwarded, custom headers may not be passed with just the default &lt;code&gt;proxy_pass&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="request-validation-marshmallow-schema"&gt;Request Validation (marshmallow Schema)
&lt;/h2&gt;&lt;p&gt;Validate the structure of the Webhook payload using a marshmallow schema:&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; only&lt;/strong&gt;: Rejects comment edit (&lt;code&gt;edited&lt;/code&gt;) or delete (&lt;code&gt;deleted&lt;/code&gt;) events.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Required field validation&lt;/strong&gt;: Returns 400 error if &lt;code&gt;comment&lt;/code&gt;, &lt;code&gt;discussion&lt;/code&gt;, &lt;code&gt;repository&lt;/code&gt; are missing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ValidationError → Audit log&lt;/strong&gt;: Invalid requests are logged in the audit log.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="input-sanitization"&gt;Input Sanitization
&lt;/h2&gt;&lt;p&gt;User comments are external input, so they must be processed:&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;User input 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;# Remove HTML tags&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;# Escape special characters&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;# Length limit&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;Where this sanitization is applied:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comment body (&lt;code&gt;comment_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Discussion title and body (&lt;code&gt;discussion_title&lt;/code&gt;, &lt;code&gt;discussion_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Original author name (&lt;code&gt;original_author&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Quoted part when posting AI response&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="username-masking"&gt;Username Masking
&lt;/h3&gt;&lt;p&gt;We do not log the full username:&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;Username masking&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;Logs will only display masked names like &lt;code&gt;yar***&lt;/code&gt;. This strikes a balance between privacy protection and debugging convenience.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="rate-limiting"&gt;Rate Limiting
&lt;/h2&gt;&lt;p&gt;Apply rate limiting per endpoint using 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 requests per minute limit&lt;/strong&gt;: A figure considering normal Webhook call frequency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;get_remote_address&lt;/code&gt;&lt;/strong&gt;: Limits based on client IP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;memory://&lt;/code&gt;&lt;/strong&gt;: In-memory storage (suitable for single process)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="systemd-security-directives"&gt;systemd Security Directives
&lt;/h2&gt;&lt;p&gt;Part 3 will cover systemd deployment in detail, but security-related directives are explained here:&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;# Security hardening&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 # Prevent privilege escalation&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 # Provide isolated /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 # Read-only file system&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 # Allow home directory access (for Claude Code config)&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;# Resource limits&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 # Memory limit&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 usage limit&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 # Process count limit&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;Directive&lt;/th&gt;
 &lt;th&gt;Effect&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;Privilege escalation via &lt;code&gt;setuid&lt;/code&gt;, &lt;code&gt;setgid&lt;/code&gt;, etc. is impossible&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;Isolated &lt;code&gt;/tmp&lt;/code&gt; namespace, separated from other processes&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;Mounts entire file system as read-only&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;Specifies only paths allowed for writing&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;Protects the entire system in OOM situations&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="conflict-between-protectsystemstrict-and-readonlypaths"&gt;Conflict between ProtectSystem=strict and ReadOnlyPaths
&lt;/h3&gt;&lt;p&gt;Initially, adding &lt;code&gt;ReadOnlyPaths=/etc/auto-comment-worker&lt;/code&gt; caused an issue where the token file could not be read. Since &lt;code&gt;ProtectSystem=strict&lt;/code&gt; already sets the entire file system to read-only, a separate &lt;code&gt;ReadOnlyPaths&lt;/code&gt; is unnecessary. It was removed because it could cause conflicts in some environments.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;In this Part 2, we covered file-based authentication management, the file permission validation journey, HMAC-SHA256 signature verification, input sanitization, rate limiting, and systemd security directives.&lt;/p&gt;
&lt;p&gt;The most important lesson in security: &lt;strong&gt;&amp;ldquo;Too strict validation is as harmful as too loose validation.&amp;rdquo;&lt;/strong&gt; The initial code, which only allowed &lt;code&gt;600&lt;/code&gt; permissions, was secure, but in the actual operating environment, it rejected files with &lt;code&gt;640&lt;/code&gt; permissions, preventing the service from starting. Focusing only on actual threats (world-writable) is the correct approach.&lt;/p&gt;
&lt;p&gt;In the next Part 3, we will cover &lt;strong&gt;Deployment and Troubleshooting&lt;/strong&gt; — systemd service configuration, nginx reverse proxy, and errors actually encountered and the resolution process.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This post is Part 2 of the AgentForge blog automatic comment system series.&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>