<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nhật Trường | DevOps, SecOps & Platforms]]></title><description><![CDATA[Nhật Trường - DevOps Engineer | Focus on GitOps workflows, DevSecOps methodologies and platforms implementations | Stay hydrated, stay deployin]]></description><link>https://blog.nh4ttruong.me</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1767598587675/30687001-b223-4320-80f8-6ccf7899a21f.png</url><title>Nhật Trường | DevOps, SecOps &amp; Platforms</title><link>https://blog.nh4ttruong.me</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 22:28:08 GMT</lastBuildDate><atom:link href="https://blog.nh4ttruong.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[DNSOps - GitOps for DNS management with DNSControl & GitLab CI/CD]]></title><description><![CDATA[This repository provides a GitOps approach to maanage your DNS records live in Git, changes are peer-reviewed, and deployments are automated through CI/CD. When the dashboard is down, your DNS config is still version-controlled and ready to push to a...]]></description><link>https://blog.nh4ttruong.me/dnsops-gitops-for-dns-management-with-dnscontrol-and-gitlab-cicd</link><guid isPermaLink="true">https://blog.nh4ttruong.me/dnsops-gitops-for-dns-management-with-dnscontrol-and-gitlab-cicd</guid><category><![CDATA[dnscontrol]]></category><category><![CDATA[gitops]]></category><category><![CDATA[dns]]></category><category><![CDATA[management]]></category><category><![CDATA[GitLab]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Tue, 13 Jan 2026 11:07:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768302672124/c25d462f-bb7d-43e7-bfb4-e429a8d6e614.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This repository provides a GitOps approach to maanage your DNS records live in Git, changes are peer-reviewed, and deployments are automated through CI/CD. When the dashboard is down, your DNS config is still version-controlled and ready to push to a backup provider.</p>
<blockquote>
<p><strong>GitOps Pattern</strong>: All DNS changes MUST be made through Git commits.Never edit DNS records directly in the provider's dashboard - changes will be overwritten on the next pipeline run.</p>
</blockquote>
<h2 id="heading-why-gitops-for-dns">Why GitOps for DNS?</h2>
<blockquote>
<p>If your DNS is only managed through a web dashboard, you're one outage away from losing control.</p>
</blockquote>
<p>Managing DNS records manually through web dashboards creates several challenges:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Challenge</td><td>GitOps Solution</td></tr>
</thead>
<tbody>
<tr>
<td><strong>No Audit Trail</strong></td><td>Full Git history - who changed what, when</td></tr>
<tr>
<td><strong>No Granular Access</strong></td><td>Branch protection, MR approvals, CODEOWNERS</td></tr>
<tr>
<td><strong>Provider Outages</strong></td><td>Config is versioned, push to backup provider</td></tr>
<tr>
<td><strong>Manual Errors</strong></td><td>Automated validation before applying</td></tr>
<tr>
<td><strong>Inconsistency</strong></td><td>Shared configs, reusable variables</td></tr>
</tbody>
</table>
</div><h2 id="heading-quick-start">Quick Start</h2>
<p>Check out <a target="_blank" href="https://github.com/nh4ttruong/dnsops">nh4ttruong/dnsops</a> or follow the steps outlined below:</p>
<h3 id="heading-1-clone-and-configure">1. Clone and Configure</h3>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/nh4ttruong/dnsops
<span class="hljs-built_in">cd</span> dnsops
</code></pre>
<h3 id="heading-2-set-cicd-variables">2. Set CI/CD Variables</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Provider</td><td>Variables</td></tr>
</thead>
<tbody>
<tr>
<td>Cloudflare</td><td><code>CF_ACCOUNT_ID_*</code>, <code>CF_API_TOKEN_*</code></td></tr>
<tr>
<td>AWS Route 53</td><td><code>AWS_ACCESS_KEY_ID</code>, <code>AWS_SECRET_ACCESS_KEY</code></td></tr>
<tr>
<td>Google Cloud</td><td><code>GCLOUD_PRIVATE_KEY</code>, <code>GCLOUD_CLIENT_EMAIL</code></td></tr>
</tbody>
</table>
</div><h3 id="heading-3-edit-dns-records">3. Edit DNS Records</h3>
<pre><code class="lang-bash">nano zones/{domain}/dnsconfig.js
</code></pre>
<h3 id="heading-4-commit-and-push">4. Commit and Push</h3>
<pre><code class="lang-bash">git add . &amp;&amp; git commit -m <span class="hljs-string">"Update DNS records"</span>
git push origin main
</code></pre>
<p>Pipeline automatically: <strong>check</strong> → <strong>preview</strong> → <strong>push</strong></p>
<hr />
<h2 id="heading-repository-structure">Repository Structure</h2>
<pre><code class="lang-bash">dns-mgmt/
├── .gitlab-ci.yml              <span class="hljs-comment"># Pipeline triggers per zone</span>
├── creds.json                  <span class="hljs-comment"># Provider credentials (env vars)</span>
└── zones/
    ├── .gitlab-ci-template.yml <span class="hljs-comment"># Shared pipeline template</span>
    ├── common.js               <span class="hljs-comment"># Shared IPs &amp; defaults</span>
    ├── example-com/
    │   └── dnsconfig.js        <span class="hljs-comment"># Zone: example.com</span>
    └── mycompany-io/
        └── dnsconfig.js        <span class="hljs-comment"># Zone: mycompany.io</span>
</code></pre>
<h3 id="heading-shared-configuration">Shared Configuration</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// zones/common.js</span>
<span class="hljs-keyword">var</span> addr = {
    <span class="hljs-string">"default"</span>: IP(<span class="hljs-string">"10.0.0.10"</span>),
    <span class="hljs-string">"private_ingress_uat"</span>: IP(<span class="hljs-string">"10.0.0.70"</span>),
    <span class="hljs-string">"public_ingress_uat"</span>: IP(<span class="hljs-string">"203.0.113.19"</span>),
    <span class="hljs-string">"public_ingress_production"</span>: IP(<span class="hljs-string">"203.0.113.21"</span>),
}
</code></pre>
<hr />
<h2 id="heading-cicd-pipeline">CI/CD Pipeline</h2>
<pre><code class="lang-mermaid">flowchart LR
    A[Git Push] --&gt; B[Trigger Zone Job]
    B --&gt; C[check]
    C --&gt; D[preview]
    D --&gt; E{Branch?}
    E --&gt;|main| F[push]
    E --&gt;|other| G[Stop]
    F --&gt; H[DNS Updated]
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Stage</td><td>Job</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>test</td><td><code>check_and_review</code></td><td>Validate syntax, preview changes</td></tr>
<tr>
<td>deploy</td><td><code>push</code></td><td>Apply to provider (main branch only)</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-provider-examples">Provider Examples</h2>
<h3 id="heading-cloudflare">Cloudflare</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// zones/example-com/dnsconfig.js</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">"../common.js"</span>);
<span class="hljs-keyword">var</span> DP = NewDnsProvider(<span class="hljs-string">"cloudflare_example_com"</span>);

D(<span class="hljs-string">"example.com"</span>, REG_NONE, DnsProvider(DP), {<span class="hljs-attr">no_ns</span>: <span class="hljs-string">'true'</span>},
  A(<span class="hljs-string">"@"</span>, addr.default),
  A(<span class="hljs-string">"www"</span>, addr.public_ingress_production),
  CNAME(<span class="hljs-string">"blog"</span>, <span class="hljs-string">"example.github.io."</span>),
);
</code></pre>
<pre><code class="lang-json"><span class="hljs-string">"cloudflare_example_com"</span>: {
    <span class="hljs-attr">"TYPE"</span>: <span class="hljs-string">"CLOUDFLAREAPI"</span>,
    <span class="hljs-attr">"accountid"</span>: <span class="hljs-string">"$CF_ACCOUNT_ID_EXAMPLE_COM"</span>,
    <span class="hljs-attr">"apitoken"</span>: <span class="hljs-string">"$CF_API_TOKEN_EXAMPLE_COM"</span>
}
</code></pre>
<h3 id="heading-aws-route-53">AWS Route 53</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// zones/example-org/dnsconfig.js</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">"../common.js"</span>);
<span class="hljs-keyword">var</span> DP = NewDnsProvider(<span class="hljs-string">"route53_example_org"</span>);

D(<span class="hljs-string">"example.org"</span>, REG_NONE, DnsProvider(DP),
  A(<span class="hljs-string">"@"</span>, addr.default),
  A(<span class="hljs-string">"api"</span>, addr.public_ingress_production),
  CNAME(<span class="hljs-string">"www"</span>, <span class="hljs-string">"example.org."</span>),
);
</code></pre>
<pre><code class="lang-json"><span class="hljs-string">"route53_example_org"</span>: {
    <span class="hljs-attr">"TYPE"</span>: <span class="hljs-string">"ROUTE53"</span>,
    <span class="hljs-attr">"KeyId"</span>: <span class="hljs-string">"$AWS_ACCESS_KEY_ID"</span>,
    <span class="hljs-attr">"SecretKey"</span>: <span class="hljs-string">"$AWS_SECRET_ACCESS_KEY"</span>
}
</code></pre>
<h3 id="heading-google-cloud-dns">Google Cloud DNS</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// zones/example-io/dnsconfig.js</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">"../common.js"</span>);
<span class="hljs-keyword">var</span> DP = NewDnsProvider(<span class="hljs-string">"gcloud_example_io"</span>);

D(<span class="hljs-string">"example.io"</span>, REG_NONE, DnsProvider(DP),
  A(<span class="hljs-string">"@"</span>, addr.default),
  AAAA(<span class="hljs-string">"@"</span>, <span class="hljs-string">"2001:db8::1"</span>),
);
</code></pre>
<pre><code class="lang-json"><span class="hljs-string">"gcloud_example_io"</span>: {
    <span class="hljs-attr">"TYPE"</span>: <span class="hljs-string">"GCLOUD"</span>,
    <span class="hljs-attr">"project"</span>: <span class="hljs-string">"my-gcp-project-id"</span>,
    <span class="hljs-attr">"private_key"</span>: <span class="hljs-string">"$GCLOUD_PRIVATE_KEY"</span>,
    <span class="hljs-attr">"client_email"</span>: <span class="hljs-string">"$GCLOUD_CLIENT_EMAIL"</span>
}
</code></pre>
<hr />
<h2 id="heading-adding-a-new-zone">Adding a New Zone</h2>
<ol>
<li><p><strong>Create zone directory</strong></p>
<pre><code class="lang-bash"> mkdir zones/new-domain-com
</code></pre>
</li>
<li><p><strong>Create dnsconfig.js</strong></p>
<pre><code class="lang-javascript"> <span class="hljs-built_in">require</span>(<span class="hljs-string">"../common.js"</span>);
 <span class="hljs-keyword">var</span> DP = NewDnsProvider(<span class="hljs-string">"cloudflare_new_domain_com"</span>);

 D(<span class="hljs-string">"new-domain.com"</span>, REG_NONE, DnsProvider(DP), {<span class="hljs-attr">no_ns</span>: <span class="hljs-string">'true'</span>},
   A(<span class="hljs-string">"@"</span>, addr.default),
 );
</code></pre>
</li>
<li><p><strong>Add credentials to creds.json</strong></p>
<pre><code class="lang-json"> <span class="hljs-string">"cloudflare_new_domain_com"</span>: {
     <span class="hljs-attr">"TYPE"</span>: <span class="hljs-string">"CLOUDFLAREAPI"</span>,
     <span class="hljs-attr">"accountid"</span>: <span class="hljs-string">"$CF_ACCOUNT_ID_NEW_DOMAIN_COM"</span>,
     <span class="hljs-attr">"apitoken"</span>: <span class="hljs-string">"$CF_API_TOKEN_NEW_DOMAIN_COM"</span>
 }
</code></pre>
</li>
<li><p><strong>Add trigger to .gitlab-ci.yml</strong></p>
<pre><code class="lang-yaml"> <span class="hljs-attr">new-domain-com:</span>
   <span class="hljs-attr">extends:</span> <span class="hljs-string">.trigger-base</span>
   <span class="hljs-attr">variables:</span>
     <span class="hljs-attr">ZONE_DIR:</span> <span class="hljs-string">new-domain-com</span>
   <span class="hljs-attr">rules:</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">changes:</span> <span class="hljs-string">*common-paths</span>
       <span class="hljs-attr">when:</span> <span class="hljs-string">manual</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">changes:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-string">zones/new-domain-com/**/*</span>
       <span class="hljs-attr">when:</span> <span class="hljs-string">always</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">when:</span> <span class="hljs-string">never</span>
</code></pre>
</li>
<li><p><strong>Set GitLab CI/CD variables</strong></p>
<ul>
<li><p><code>CF_ACCOUNT_ID_NEW_DOMAIN_COM</code></p>
</li>
<li><p><code>CF_API_TOKEN_NEW_DOMAIN_COM</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-local-development">Local Development</h2>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> zones/{domain}

dnscontrol check                              <span class="hljs-comment"># Validate syntax</span>
dnscontrol preview --creds ../../creds.json   <span class="hljs-comment"># Preview changes</span>
dnscontrol push --creds ../../creds.json      <span class="hljs-comment"># Apply changes</span>
</code></pre>
<hr />
<h2 id="heading-supported-providers">Supported Providers</h2>
<p>DNSControl supports <strong>40+ providers</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Provider</td><td>Type</td><td>Best For</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/cloudflareapi">Cloudflare</a></td><td>CDN + DNS</td><td>Proxy, WAF, DDoS protection</td></tr>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/route53">AWS Route 53</a></td><td>Cloud DNS</td><td>AWS ecosystem integration</td></tr>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/gcloud">Google Cloud DNS</a></td><td>Cloud DNS</td><td>GCP workloads</td></tr>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/azuredns">Azure DNS</a></td><td>Cloud DNS</td><td>Azure ecosystem</td></tr>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/digitalocean">DigitalOcean</a></td><td>Cloud DNS</td><td>Simple, affordable</td></tr>
<tr>
<td><a target="_blank" href="https://docs.dnscontrol.org/provider/namecheap">Namecheap</a></td><td>Registrar</td><td>Domain + DNS bundled</td></tr>
</tbody>
</table>
</div><blockquote>
<p>Full list: <a target="_blank" href="http://docs.dnscontrol.org/provider">docs.dnscontrol.org/provider</a></p>
</blockquote>
<hr />
<h2 id="heading-multi-provider-strategy">Multi-Provider Strategy</h2>
<p>Manage multiple providers in one repository:</p>
<ul>
<li><p><strong>Migration</strong> - Move zones between providers gradually</p>
</li>
<li><p><strong>Redundancy</strong> - Backup zones on secondary providers</p>
</li>
<li><p><strong>Cost optimization</strong> - Different providers for different needs</p>
</li>
</ul>
<pre><code class="lang-mermaid">flowchart TB
    subgraph repo["dnsops"]
        common[zones/common.js]
        zone1[zones/main-domain/]
        zone2[zones/backup-domain/]
        zone3[zones/legacy-domain/]
    end

    zone1 --&gt; cf[Cloudflare]
    zone2 --&gt; r53[AWS Route 53]
    zone3 --&gt; gcp[Google Cloud DNS]
</code></pre>
<hr />
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://docs.dnscontrol.org/">DNSControl Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.dnscontrol.org/provider">Provider Reference</a></p>
</li>
<li><p><a target="_blank" href="https://docs.dnscontrol.org/language-reference">Language Reference</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Kubernetes API Server & Kubelet Performance Testing]]></title><description><![CDATA[In Kubernetes context, while the API Server is the brain, the Kubelet is the muscle that actually runs your workloads. Both need to be stress-tested to guarantee successful deployments.
To test this, ]]></description><link>https://blog.nh4ttruong.me/kubernetes-api-server-and-kubelet-performance-testing</link><guid isPermaLink="true">https://blog.nh4ttruong.me/kubernetes-api-server-and-kubelet-performance-testing</guid><category><![CDATA[KubeBurner]]></category><category><![CDATA[control plane]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Benchmark]]></category><category><![CDATA[Performance Testing]]></category><category><![CDATA[kubelet]]></category><category><![CDATA[kube-apiserver]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Fri, 09 Jan 2026 04:16:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767932161025/1ba5b359-b368-4b39-affc-d1459812ceef.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Kubernetes context, while the API Server is the brain, the Kubelet is the muscle that actually runs your workloads. Both need to be stress-tested to guarantee successful deployments.</p>
<p>To test this, we use <strong>kube-burner</strong>, a tool designed to stress the control plane by creating, updating, and deleting thousands of objects, while simultaneously measuring how fast Kubelets can pick up and run these workloads.</p>
<h2><strong>Why Test?</strong></h2>
<p>The API Server is the central point for all requests in a Kubernetes cluster. If the API Server cannot handle the load, the entire cluster is affected:</p>
<ul>
<li><p>Pod scheduling becomes slow or fails</p>
</li>
<li><p>Service discovery stops working</p>
</li>
<li><p>kubectl commands timeout</p>
</li>
<li><p>Controllers cannot reconcile</p>
</li>
</ul>
<p><strong>Context</strong>: When a cluster has many workloads, the API Server must handle thousands of requests/second from:</p>
<ul>
<li><p>Controllers (deployment, replicaset, daemonset...)</p>
</li>
<li><p>Kubelet (node status, pod status)</p>
</li>
<li><p>Users (kubectl, CI/CD pipelines)</p>
</li>
<li><p>Applications (in-cluster clients)</p>
</li>
</ul>
<h2><strong>Install kube-burner</strong></h2>
<pre><code class="language-shell"># Download and install
curl -L https://github.com/cloud-bulldozer/kube-burner/releases/latest/download/kube-burner-linux-amd64.tar.gz | tar xz
sudo mv kube-burner /usr/local/bin/

# Verify
kube-burner version
</code></pre>
<p>Reference: <a href="https://kube-burner.readthedocs.io/en/latest/installation/">kube-burner installation</a></p>
<h2><strong>Test Scenarios</strong></h2>
<h3><strong>1. Smoke Test</strong></h3>
<p><strong>Purpose</strong>: Basic validation, baseline performance.</p>
<p><strong>Input</strong>:</p>
<ul>
<li><p>10 namespaces</p>
</li>
<li><p>50 objects (secrets, deployments)</p>
</li>
<li><p>QPS: 5, Burst: 5</p>
</li>
</ul>
<p><strong>Run test</strong>:</p>
<pre><code class="language-shell">kube-burner init -c api-server/smoke.yaml
</code></pre>
<p><strong>Expected output</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Metric</strong></th>
<th><strong>Target</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Success rate</td>
<td>&gt; 99%</td>
</tr>
<tr>
<td>P99 latency</td>
<td>&lt; 500ms</td>
</tr>
<tr>
<td>Duration</td>
<td>~3-5 min</td>
</tr>
</tbody></table>
<h3><strong>2. Load Test (API Intensive)</strong></h3>
<p><strong>Purpose</strong>: Evaluate API Server capacity under production load.</p>
<p><strong>Input</strong>:</p>
<ul>
<li><p>30 namespaces</p>
</li>
<li><p>1,800 objects (deployments, configmaps, secrets, services)</p>
</li>
<li><p>QPS: 25, Burst: 30</p>
</li>
<li><p>3 phases: Create → Patch → Delete</p>
</li>
</ul>
<p><strong>Run test</strong>:</p>
<pre><code class="language-shell">kube-burner init -c api-server/api-intensive.yml
</code></pre>
<p><strong>Test phases</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Phase</strong></th>
<th><strong>Action</strong></th>
<th><strong>Duration</strong></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Object creation (1,800 objects)</td>
<td>~15 min</td>
</tr>
<tr>
<td>2</td>
<td>Object patching (JSON patch, strategic merge)</td>
<td>~5 min</td>
</tr>
<tr>
<td>3</td>
<td>Cleanup (cascade delete)</td>
<td>~10 min</td>
</tr>
</tbody></table>
<p><strong>Expected output</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Cluster Size</strong></th>
<th><strong>API Server QPS</strong></th>
<th><strong>P99 Latency</strong></th>
</tr>
</thead>
<tbody><tr>
<td>3-5 nodes</td>
<td>500-1,500</td>
<td>&lt; 1s</td>
</tr>
<tr>
<td>5-20 nodes</td>
<td>1,500-5,000</td>
<td>&lt; 500ms</td>
</tr>
<tr>
<td>20+ nodes</td>
<td>5,000-15,000</td>
<td>&lt; 200ms</td>
</tr>
</tbody></table>
<h3><strong>3. Kubelet Density Test</strong></h3>
<p><strong>Purpose</strong>: Evaluate cluster scheduling and pod lifecycle capabilities.</p>
<p><strong>Run test</strong>:</p>
<pre><code class="language-shell"># Web application workload
kube-burner init -c kubelet-density-cni/kubelet-density-cni.yml

# Database workload  
kube-burner init -c kubelet-density-database/kubelet-density-database.yml
</code></pre>
<p><strong>Expected output</strong>:</p>
<table>
<thead>
<tr>
<th><strong>Metric</strong></th>
<th><strong>Target</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Pod startup time</td>
<td>&lt; 30s</td>
</tr>
<tr>
<td>Scheduling latency</td>
<td>&lt; 5s</td>
</tr>
<tr>
<td>Pod churn rate</td>
<td>Stable</td>
</tr>
</tbody></table>
<h2><strong>Metrics to Monitor</strong></h2>
<p>In Grafana (import <code>grafana-dashboard/k8s-system-api-server.json</code>):</p>
<table>
<thead>
<tr>
<th><strong>Metric</strong></th>
<th><strong>PromQL</strong></th>
<th><strong>Meaning</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Request latency</td>
<td><code>histogram_quantile(0.99, apiserver_request_duration_seconds_bucket)</code></td>
<td>P99 response time</td>
</tr>
<tr>
<td>Request rate</td>
<td><code>sum(rate(apiserver_request_total[5m]))</code></td>
<td>QPS</td>
</tr>
<tr>
<td>Error rate</td>
<td><code>sum(rate(apiserver_request_total{code=~"5.."}[5m]))</code></td>
<td>Server errors</td>
</tr>
<tr>
<td>etcd latency</td>
<td><code>histogram_quantile(0.99, etcd_request_duration_seconds_bucket)</code></td>
<td>Backend latency</td>
</tr>
</tbody></table>
<h2><strong>Parameter Tuning</strong></h2>
<p>Adjust in config file based on cluster size:</p>
<pre><code class="language-yaml"># Small cluster (3-5 nodes)
jobs:
  - name: api-test
    qps: 10
    burst: 20
    jobIterations: 10
    replicas: 5

# Large cluster (20+ nodes)
jobs:
  - name: api-test
    qps: 100
    burst: 200
    jobIterations: 50
    replicas: 50
</code></pre>
<h2><strong>Troubleshooting</strong></h2>
<p><strong>Pods stuck Pending</strong>:</p>
<p>→ Reduce <code>replicas</code> or increase cluster resources.</p>
<p><strong>High API latency</strong>:</p>
<pre><code class="language-shell">kubectl top pods -n kube-system
kubectl logs kube-apiserver-&lt;node&gt; -n kube-system | grep -i error
</code></pre>
<p>→ Check etcd performance, reduce <code>qps/burst</code>.</p>
<p><strong>Cleanup failed</strong>:</p>
<pre><code class="language-shell"># Get list of namespaces to delete
kubectl get namespace -l kube-burner-job=&lt;job-name&gt;

# Delete each namespace
kubectl delete namespace &lt;namespace-name&gt;

# Or use xargs to delete in bulk
kubectl get namespace -l kube-burner-job=&lt;job-name&gt; -o name | xargs kubectl delete
</code></pre>
<h2><strong>References</strong></h2>
<ul>
<li><p><a href="https://kube-burner.readthedocs.io/">kube-burner Documentation</a></p>
</li>
<li><p><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/">Kubernetes API Server</a></p>
</li>
</ul>
<hr />
<p>%<a class="embed-card" href="nh4ttruong">nh4ttruong</a></p>
]]></content:encoded></item><item><title><![CDATA[Benchmarking etcd: The Heartbeat of Kubernetes]]></title><description><![CDATA[Welcome to the "heartbeat" of Kubernetes. How do you benchmark and perform performance testing? Today, we focus on etcd.
Etcd is the consistent and highly available key-value store used as Kubernetes' backing store for all cluster data. If etcd is sl...]]></description><link>https://blog.nh4ttruong.me/benchmarking-etcd-the-heartbeat-of-kubernetes</link><guid isPermaLink="true">https://blog.nh4ttruong.me/benchmarking-etcd-the-heartbeat-of-kubernetes</guid><category><![CDATA[etcd]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Databases]]></category><category><![CDATA[performance]]></category><category><![CDATA[Benchmark]]></category><category><![CDATA[Performance Testing]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Fri, 09 Jan 2026 03:59:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767930669177/b4f4e5c8-a6d2-46bf-b444-dd3af0a4ad2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the "heartbeat" of Kubernetes. How do you benchmark and perform performance testing? Today, we focus on <strong>etcd</strong>.</p>
<p>Etcd is the consistent and highly available key-value store used as Kubernetes' backing store for all cluster data. If <code>etcd</code> is slow, your entire cluster feels sluggish. API requests time out, and controllers fail to sync.</p>
<h2 id="heading-about-etcd">🧠 About etcd</h2>
<p>To optimize something, you must first understand how it works. <strong>etcd</strong> is a distributed, consistent key-value store that serves as the "brain" of your Kubernetes cluster.</p>
<h3 id="heading-how-it-works">How it Works</h3>
<p>Etcd uses the <strong>Raft consensus algorithm</strong> to ensure data consistency across the cluster (typically 3 or 5 nodes).</p>
<ol>
<li><p><strong>Leader Election</strong>: One node is elected leader; all writes must go through it.</p>
</li>
<li><p><strong>Replication</strong>: The leader replicates the log entry to followers.</p>
</li>
<li><p><strong>Persistence (The Bottleneck)</strong>: Before confirming a write, etcd must persist the data to disk (Write-Ahead Log or WAL) using an <code>fsync</code> system call.</p>
</li>
<li><p><strong>Consensus</strong>: Once a majority (quorum) confirms the write, the request succeeds.</p>
</li>
</ol>
<p>Because every state change requires an <code>fsync</code> to disk, <strong>disk latency is the critical path</strong>. If your disk is slow, <code>fsync</code> takes longer, the leader blocks, and the entire Kubernetes control plane slows down. This is why "fast SSDs" are non-negotiable for etcd.</p>
<h2 id="heading-why-test">Why Test?</h2>
<p>etcd stores the entire state of a Kubernetes cluster:</p>
<ul>
<li><p>Cluster configuration</p>
</li>
<li><p>Pod/Service/Deployment definitions</p>
</li>
<li><p>Secrets and ConfigMaps</p>
</li>
<li><p>Custom Resources</p>
</li>
</ul>
<p>If etcd is slow, the entire cluster is affected:</p>
<ul>
<li><p>API Server response becomes slow</p>
</li>
<li><p>Controllers cannot update state</p>
</li>
<li><p>Pod scheduling is delayed</p>
</li>
<li><p>Watch operations timeout</p>
</li>
</ul>
<p><strong>Context</strong>: etcd is a single point of failure. etcd performance determines the performance of the entire cluster.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<h3 id="heading-access-methods">Access Methods</h3>
<p><strong>Option 1</strong>: SSH into control plane node (recommended for production)</p>
<pre><code class="lang-bash">ssh control-plane-node
</code></pre>
<p><strong>Option 2</strong>: kubectl exec into etcd pod</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it etcd-&lt;node-name&gt; -n kube-system -- sh
</code></pre>
<h3 id="heading-install-tools">Install Tools</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Download etcd binaries</span>
ETCD_VER=v3.5.4
curl -L https://github.com/etcd-io/etcd/releases/download/<span class="hljs-variable">${ETCD_VER}</span>/etcd-<span class="hljs-variable">${ETCD_VER}</span>-linux-amd64.tar.gz -o /tmp/etcd.tar.gz
tar xzvf /tmp/etcd.tar.gz -C /tmp --strip-components=1

<span class="hljs-comment"># Install tools</span>
sudo mv /tmp/etcdctl /usr/<span class="hljs-built_in">local</span>/bin/
sudo mv /tmp/benchmark /usr/<span class="hljs-built_in">local</span>/bin/

<span class="hljs-comment"># Verify</span>
etcdctl version
</code></pre>
<h3 id="heading-etcd-endpoints-and-certificates">etcd Endpoints and Certificates</h3>
<p>Get information from kubeadm cluster:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Endpoints</span>
kubectl get endpoints -n kube-system etcd -o jsonpath=<span class="hljs-string">'{.subsets[*].addresses[*].ip}'</span>

<span class="hljs-comment"># Certificates (usually located here)</span>
/etc/kubernetes/pki/etcd/
├── ca.crt
├── server.crt
└── server.key
</code></pre>
<p>Reference: <a target="_blank" href="https://etcd.io/docs/v3.5/op-guide/security/">etcd security</a></p>
<h2 id="heading-test-scenarios">Test Scenarios</h2>
<h3 id="heading-1-smoke-test">1. Smoke Test</h3>
<p><strong>Purpose</strong>: Verify connectivity and basic operations.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Set environment variables</span>
<span class="hljs-built_in">export</span> ETCDCTL_API=3
<span class="hljs-built_in">export</span> ETCDCTL_ENDPOINTS=<span class="hljs-string">"https://10.10.10.1:2379,https://10.10.10.2:2379,https://10.10.10.3:2379"</span>
<span class="hljs-built_in">export</span> ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
<span class="hljs-built_in">export</span> ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
<span class="hljs-built_in">export</span> ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key

<span class="hljs-comment"># Run smoke test</span>
./scripts/etcdctl/smoke.sh
</code></pre>
<p><strong>Script performs</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Member list</span>
etcdctl member list

<span class="hljs-comment"># Endpoint health</span>
etcdctl endpoint health

<span class="hljs-comment"># Endpoint status</span>
etcdctl endpoint status

<span class="hljs-comment"># Basic put/get/delete</span>
etcdctl put /perf-test/key <span class="hljs-string">"value"</span>
etcdctl get /perf-test/key
etcdctl del /perf-test/key
</code></pre>
<p><strong>Expected output</strong>:</p>
<ul>
<li><p>All members healthy</p>
</li>
<li><p>1 leader present</p>
</li>
<li><p>Put/get/delete successful</p>
</li>
</ul>
<h3 id="heading-2-etcdctl-benchmark">2. etcdctl Benchmark</h3>
<p><strong>Purpose</strong>: Measure client-side performance with realistic load patterns.</p>
<pre><code class="lang-bash">./scripts/etcdctl/etcdctl-tool.sh
</code></pre>
<p><strong>Test phases</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Phase</td><td>Rate</td><td>Clients</td><td>Duration</td></tr>
</thead>
<tbody>
<tr>
<td>Medium write</td><td>1,000 req/s</td><td>200</td><td>60s</td></tr>
<tr>
<td>Heavy write</td><td>8,000 req/s</td><td>500</td><td>60s</td></tr>
<tr>
<td>Heavy read</td><td>15,000 req/s</td><td>1,000</td><td>60s</td></tr>
</tbody>
</table>
</div><p><strong>Expected output</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>3-node cluster</td><td>5+ node cluster</td></tr>
</thead>
<tbody>
<tr>
<td>Write latency P99</td><td>&lt; 50ms</td><td>&lt; 25ms</td></tr>
<tr>
<td>Read latency P99</td><td>&lt; 15ms</td><td>&lt; 5ms</td></tr>
<tr>
<td>Write throughput</td><td>5k-10k req/s</td><td>15k-30k req/s</td></tr>
<tr>
<td>Read throughput</td><td>20k-50k req/s</td><td>80k-150k req/s</td></tr>
</tbody>
</table>
</div><h3 id="heading-3-benchmark-tool-test">3. Benchmark Tool Test</h3>
<p><strong>Purpose</strong>: Measure raw database performance (server-side).</p>
<pre><code class="lang-bash">./scripts/benchmark/benchmark-tool.sh
</code></pre>
<p><strong>Test types</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Test</td><td>Description</td><td>Command</td></tr>
</thead>
<tbody>
<tr>
<td>Sequential write</td><td>Single client writes</td><td><code>benchmark --conns=1 --clients=1</code></td></tr>
<tr>
<td>Concurrent write</td><td>Multi-client writes</td><td><code>benchmark --conns=100 --clients=1000</code></td></tr>
<tr>
<td>Read (linearizable)</td><td>Strong consistency reads</td><td><code>benchmark --consistency=l</code></td></tr>
<tr>
<td>Read (serializable)</td><td>Weak consistency reads</td><td><code>benchmark --consistency=s</code></td></tr>
</tbody>
</table>
</div><h3 id="heading-cleanup">Cleanup</h3>
<p>After testing, delete test data:</p>
<pre><code class="lang-bash">./scripts/benchmark/clean-and-recovery.sh
</code></pre>
<h2 id="heading-metrics-to-monitor">Metrics to Monitor</h2>
<p>In Grafana (import <code>grafana-dashboard/k8s-system-etcd.json</code>):</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>PromQL</td><td>Meaning</td></tr>
</thead>
<tbody>
<tr>
<td>WAL fsync duration</td><td><code>histogram_quantile(0.99, etcd_disk_wal_fsync_duration_seconds_bucket)</code></td><td>Disk write performance</td></tr>
<tr>
<td>Backend commit</td><td><code>histogram_quantile(0.99, etcd_disk_backend_commit_duration_seconds_bucket)</code></td><td>Database commit time</td></tr>
<tr>
<td>Leader elections</td><td><code>increase(etcd_server_leader_changes_seen_total[1h])</code></td><td>Cluster stability</td></tr>
<tr>
<td>DB size</td><td><code>etcd_mvcc_db_total_size_in_bytes</code></td><td>Database size</td></tr>
</tbody>
</table>
</div><p><strong>Thresholds</strong>:</p>
<ul>
<li><p>WAL fsync P99 &gt; 10ms → Disk too slow</p>
</li>
<li><p>Leader changes &gt; 0/hour → Network or disk issues</p>
</li>
<li><p>DB size &gt; 6GB → Need compact/defrag</p>
</li>
</ul>
<p>Reference: <a target="_blank" href="https://etcd.io/docs/v3.5/metrics/">etcd metrics</a></p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p><strong>Connection refused</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check endpoints</span>
etcdctl endpoint health
<span class="hljs-comment"># Verify firewall, certificates</span>
</code></pre>
<p><strong>High latency</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check disk I/O</span>
iostat -x 1

<span class="hljs-comment"># Check etcd logs</span>
kubectl logs etcd-&lt;node&gt; -n kube-system | grep -i slow
</code></pre>
<p><strong>Database too large</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Compact and defrag</span>
etcdctl compact $(etcdctl get / --<span class="hljs-built_in">limit</span> 1 --rev | head -1)
etcdctl defrag
</code></pre>
<h2 id="heading-configuration">Configuration</h2>
<p>Update scripts with cluster info:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># scripts/etcdctl/etcdctl-tool.sh</span>
ETCD_HOSTS=<span class="hljs-string">"10.10.10.1:2379,10.10.10.2:2379,10.10.10.3:2379"</span>
OPTIONS=<span class="hljs-string">"--cacert=/path/to/ca.crt --cert=/path/to/client.crt --key=/path/to/client.key"</span>
</code></pre>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://etcd.io/docs/">etcd Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/etcd-io/etcd/tree/main/tools/benchmark">etcd Benchmark Tool</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/">Kubernetes etcd Best Practices</a></p>
</li>
<li><p><a target="_blank" href="https://etcd.io/docs/v3.5/op-guide/performance/">etcd Performance</a></p>
</li>
</ul>
<hr />
<div class="hn-embed-widget" id="nh4ttruong"></div>]]></content:encoded></item><item><title><![CDATA[Building a Production-Ready Kubernetes Performance Testing Framework]]></title><description><![CDATA[Building a Kubernetes cluster is easy; proving it's production-ready is hard. How do you know if your control plane can scale? Is your storage actually delivering the IOPS promised by the vendor?
To answer these questions and ensure the cluster is re...]]></description><link>https://blog.nh4ttruong.me/building-a-production-ready-kubernetes-performance-testing-framework</link><guid isPermaLink="true">https://blog.nh4ttruong.me/building-a-production-ready-kubernetes-performance-testing-framework</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Performance Testing]]></category><category><![CDATA[SRE]]></category><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Fri, 09 Jan 2026 03:48:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767929670794/4ca5d54b-d846-4fd2-9a7e-ff958443c73d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building a Kubernetes cluster is easy; proving it's production-ready is hard. How do you know if your control plane can scale? Is your storage actually delivering the IOPS promised by the vendor?</p>
<p>To answer these questions and ensure the cluster is ready for production, I researched and gathered information on setting it up. Then, I built a performance test framework to do it.</p>
<p>In this post, I'll walk you through the <strong>Objective</strong>, the <strong>Test Flow</strong>, and the <strong>Requirements</strong> needed to set up this framework.</p>
<blockquote>
<p>TL;DR 👉 <a target="_blank" href="https://github.com/nh4ttruong/k8s-perf-tests">github.com/nh4ttruong/k8s-perf-tests</a></p>
</blockquote>
<h2 id="heading-the-objective">🎯 The Objective</h2>
<p>The goal is to ensure the Kubernetes cluster meets specific performance requirements across all layers:</p>
<ul>
<li><p><strong>Control Plane</strong>: API Server handles expected request volume.</p>
</li>
<li><p><strong>etcd</strong>: Database meets throughput and latency requirements.</p>
</li>
<li><p><strong>Network</strong>: Pod-to-pod communication achieves expected bandwidth and latency.</p>
</li>
<li><p><strong>DNS</strong>: CoreDNS handles query rate from services.</p>
</li>
<li><p><strong>Storage</strong>: Persistent Volume meets IOPS and throughput requirements.</p>
</li>
<li><p><strong>Ingress</strong>: Load balancer handles external traffic.</p>
</li>
</ul>
<h2 id="heading-the-test-flow">🔄 The Test Flow</h2>
<p>We don't test randomly. We execute in a specific order, moving from the infrastructure layer up to the application layer. If the foundation (<code>etcd</code>) is shaky, testing the Ingress is pointless.</p>
<ol>
<li><p><strong>etcd</strong> → Database performance (foundation for everything)</p>
</li>
<li><p><strong>API Server</strong> → Control plane capacity</p>
</li>
<li><p><strong>Network (CNI)</strong> → Pod networking performance</p>
</li>
<li><p><strong>CoreDNS</strong> → Service discovery latency</p>
</li>
<li><p><strong>Storage</strong> → Persistent volume I/O</p>
</li>
<li><p><strong>Ingress</strong> → External traffic handling</p>
</li>
</ol>
<h2 id="heading-components-amp-tools">🛠 Components &amp; Tools</h2>
<p>Here is the stack we use to validate each component. Click on the component name to view the source code and detailed documentation:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Tool</td><td>Test Focus</td></tr>
</thead>
<tbody>
<tr>
<td><strong>API Server &amp; Kubelet</strong></td><td><code>kube-burner</code></td><td>Object CRUD, pod scheduling</td></tr>
<tr>
<td><strong>etcd</strong></td><td><code>etcdctl</code>, <code>benchmark</code></td><td>Read/write latency, throughput</td></tr>
<tr>
<td><strong>CoreDNS</strong></td><td><code>dnsperf</code></td><td>Query throughput, latency</td></tr>
<tr>
<td><strong>Network</strong></td><td><code>k8s-netperf</code></td><td>Pod-to-pod, service latency</td></tr>
<tr>
<td><strong>Storage</strong></td><td><code>fio</code>, <code>kbench</code></td><td>IOPS, throughput, latency</td></tr>
<tr>
<td><strong>Ingress</strong></td><td><code>wrk</code></td><td>HTTP RPS, response time</td></tr>
<tr>
<td><strong>Monitoring</strong></td><td><code>Grafana</code></td><td>Real-time metrics</td></tr>
</tbody>
</table>
</div><h2 id="heading-test-types">Test types</h2>
<p>For each component in the following posts, we will look at three types of tests:</p>
<ol>
<li><p><strong>Smoke</strong>: Validate configuration (1-5 min).</p>
</li>
<li><p><strong>Load</strong>: Measure performance at expected load (15-60 min, 70-100% capacity).</p>
</li>
<li><p><strong>Stress</strong>: Find the breaking point (10-30 min, 150-200% capacity).</p>
</li>
</ol>
<h2 id="heading-quick-start">Quick Start</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># Clone repository</span>
git <span class="hljs-built_in">clone</span> https://github.com/nh4ttruong/k8s-perf-test.git
<span class="hljs-built_in">cd</span> k8s-perf-test

<span class="hljs-comment"># 1. etcd smoke test (if you have access)</span>
./etcd/scripts/etcdctl/smoke.sh

<span class="hljs-comment"># 2. API Server smoke test</span>
kube-burner init -c kube-burner/api-server/smoke.yaml

<span class="hljs-comment"># 3. Network smoke test</span>
k8s-netperf --config network/smoke_1.yaml --<span class="hljs-built_in">local</span>

<span class="hljs-comment"># 4. DNS smoke test</span>
kubectl apply -f coredns/
kubectl logs -f -l app=dnsperf -n coredns-perf-test

<span class="hljs-comment"># 5. Storage smoke test</span>
kubectl create namespace storage-perf-test
kubectl apply -f storage/smoke.yaml -n storage-perf-test
</code></pre>
<h2 id="heading-evaluation-criteria">Evaluation Criteria</h2>
<h3 id="heading-kubernetes-slisslos">Kubernetes SLIs/SLOs</h3>
<p>The Kubernetes project defines Service Level Indicators (SLIs) and Service Level Objectives (SLOs) for a properly functioning cluster. These criteria are used for performance evaluation.</p>
<p>Reference: <a target="_blank" href="https://github.com/kubernetes/community/blob/master/sig-scalability/slos/slos.md">Kubernetes Scalability SLIs/SLOs</a></p>
<h4 id="heading-api-server-slos">API Server SLOs</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>SLI</td><td>SLO</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>Mutating API latency (P99)</td><td>≤ 1s</td><td>Time to process CREATE, UPDATE, DELETE</td></tr>
<tr>
<td>Non-mutating API latency (P99)</td><td>≤ 1s (single object)</td><td>Time to process GET single resource</td></tr>
<tr>
<td>Non-mutating API latency (P99)</td><td>≤ 30s (list objects)</td><td>Time to process LIST resources</td></tr>
</tbody>
</table>
</div><h4 id="heading-pod-startup-slos">Pod Startup SLOs</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>SLI</td><td>SLO</td><td>Condition</td></tr>
</thead>
<tbody>
<tr>
<td>Pod startup latency (P99)</td><td>≤ 5s</td><td>Stateless pods, image already present</td></tr>
<tr>
<td>Pod startup latency (P99)</td><td>≤ 20s</td><td>Stateless pods, image pull required</td></tr>
</tbody>
</table>
</div><h3 id="heading-target-metrics-by-component">Target Metrics by Component</h3>
<p>Specific evaluation criteria for each component:</p>
<h4 id="heading-control-plane">Control Plane</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Metric</td><td>Target</td><td>Critical</td><td>Reference</td></tr>
</thead>
<tbody>
<tr>
<td>API Server</td><td>Mutating P99</td><td>&lt; 500ms</td><td>&lt; 1s</td><td><a target="_blank" href="https://github.com/kubernetes/community/blob/master/sig-scalability/slos/slos.md">K8s SLOs</a></td></tr>
<tr>
<td>API Server</td><td>Non-mutating P99</td><td>&lt; 200ms</td><td>&lt; 1s</td><td><a target="_blank" href="https://github.com/kubernetes/community/blob/master/sig-scalability/slos/slos.md">K8s SLOs</a></td></tr>
<tr>
<td>API Server</td><td>QPS sustained</td><td>\&gt; 1000</td><td>\&gt; 500</td><td>Depends on cluster size</td></tr>
<tr>
<td>API Server</td><td>Error rate</td><td>&lt; 0.1%</td><td>&lt; 1%</td><td></td></tr>
<tr>
<td>etcd</td><td>Write latency P99</td><td>&lt; 25ms</td><td>&lt; 50ms</td><td><a target="_blank" href="https://etcd.io/docs/v3.5/tuning/">etcd tuning</a></td></tr>
<tr>
<td>etcd</td><td>Read latency P99</td><td>&lt; 10ms</td><td>&lt; 25ms</td><td><a target="_blank" href="https://etcd.io/docs/v3.5/tuning/">etcd tuning</a></td></tr>
<tr>
<td>etcd</td><td>fsync duration P99</td><td>&lt; 10ms</td><td>&lt; 25ms</td><td><a target="_blank" href="https://etcd.io/docs/v3.5/op-guide/hardware/">etcd hardware</a></td></tr>
</tbody>
</table>
</div><h4 id="heading-data-plane">Data Plane</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Metric</td><td>Target</td><td>Critical</td><td>Reference</td></tr>
</thead>
<tbody>
<tr>
<td>Pod-to-pod</td><td>Throughput</td><td>\&gt; 5 Gbps</td><td>\&gt; 1 Gbps</td><td>Depends on physical network</td></tr>
<tr>
<td>Pod-to-pod</td><td>Latency</td><td>&lt; 1ms</td><td>&lt; 5ms</td><td>Same zone</td></tr>
<tr>
<td>Pod-to-service</td><td>Latency</td><td>&lt; 2ms</td><td>&lt; 10ms</td><td>Via kube-proxy</td></tr>
<tr>
<td>CoreDNS</td><td>Query rate</td><td>\&gt; 10k QPS</td><td>\&gt; 5k QPS</td><td><a target="_blank" href="https://coredns.io/plugins/cache/">CoreDNS plugins</a></td></tr>
<tr>
<td>CoreDNS</td><td>P99 latency</td><td>&lt; 10ms</td><td>&lt; 50ms</td><td>With cache</td></tr>
<tr>
<td>CoreDNS</td><td>Cache hit ratio</td><td>\&gt; 90%</td><td>\&gt; 80%</td></tr>
</tbody>
</table>
</div><h4 id="heading-storage">Storage</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Workload Type</td><td>Metric</td><td>SSD Target</td><td>NVMe Target</td><td>Reference</td></tr>
</thead>
<tbody>
<tr>
<td>Database (OLTP)</td><td>Random 4K read IOPS</td><td>\&gt; 10k</td><td>\&gt; 50k</td><td><a target="_blank" href="https://fio.readthedocs.io/">fio profiles</a></td></tr>
<tr>
<td>Database (OLTP)</td><td>Random 4K write IOPS</td><td>\&gt; 5k</td><td>\&gt; 20k</td><td></td></tr>
<tr>
<td>Database (OLTP)</td><td>P99 latency</td><td>&lt; 5ms</td><td>&lt; 1ms</td><td></td></tr>
<tr>
<td>Logging/Streaming</td><td>Sequential write MB/s</td><td>\&gt; 200</td><td>\&gt; 1000</td><td></td></tr>
<tr>
<td>Analytics (OLAP)</td><td>Sequential read MB/s</td><td>\&gt; 300</td><td>\&gt; 2000</td></tr>
</tbody>
</table>
</div><h4 id="heading-ingress">Ingress</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>Small cluster</td><td>Large cluster</td><td>Reference</td></tr>
</thead>
<tbody>
<tr>
<td>Requests/sec</td><td>\&gt; 10k</td><td>\&gt; 50k</td><td><a target="_blank" href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/">NGINX tuning</a></td></tr>
<tr>
<td>P99 latency</td><td>&lt; 100ms</td><td>&lt; 20ms</td><td></td></tr>
<tr>
<td>Error rate (5xx)</td><td>&lt; 0.1%</td><td>&lt; 0.01%</td><td></td></tr>
<tr>
<td>Connection rate</td><td>\&gt; 5k/s</td><td>\&gt; 20k/s</td></tr>
</tbody>
</table>
</div><h3 id="heading-result-evaluation">Result Evaluation</h3>
<h4 id="heading-passfail-criteria">Pass/Fail Criteria</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Result</td><td>Condition</td></tr>
</thead>
<tbody>
<tr>
<td><strong>PASS</strong></td><td>All metrics meet Target</td></tr>
<tr>
<td><strong>CONDITIONAL PASS</strong></td><td>All metrics within Critical range, some not meeting Target</td></tr>
<tr>
<td><strong>FAIL</strong></td><td>Any metric exceeds Critical threshold</td></tr>
</tbody>
</table>
</div><h4 id="heading-pre-production-checklist">Pre-Production Checklist</h4>
<ul>
<li><p>API Server P99 latency &lt; 500ms under expected load</p>
</li>
<li><p>etcd write latency P99 &lt; 25ms</p>
</li>
<li><p>No etcd leader election during 24h test</p>
</li>
<li><p>Pod startup time P99 &lt; 5s (image cached)</p>
</li>
<li><p>DNS query latency P99 &lt; 10ms</p>
</li>
<li><p>Storage IOPS meets workload requirements</p>
</li>
<li><p>Network throughput meets inter-zone requirements</p>
</li>
<li><p>Ingress handles expected peak traffic</p>
</li>
</ul>
<h3 id="heading-references">References</h3>
<ul>
<li><p><a target="_blank" href="https://github.com/kubernetes/community/blob/master/sig-scalability/slos/slos.md">Kubernetes Scalability SLIs/SLOs</a> - Official SLO definitions</p>
</li>
<li><p><a target="_blank" href="https://github.com/kubernetes/community/blob/master/sig-scalability/configs-and-limits/thresholds.md">Kubernetes Scalability Thresholds</a> - Cluster size limits</p>
</li>
<li><p><a target="_blank" href="https://github.com/kubernetes/community/tree/master/sig-scalability">SIG Scalability</a> - Scalability working group</p>
</li>
<li><p><a target="_blank" href="https://kube-burner.readthedocs.io/">kube-burner Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://etcd.io/docs/v3.5/op-guide/performance/">etcd Performance</a></p>
</li>
<li><p><a target="_blank" href="https://etcd.io/docs/v3.5/tuning/">etcd Tuning</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/cloud-bulldozer/k8s-netperf">k8s-netperf</a></p>
</li>
<li><p><a target="_blank" href="https://fio.readthedocs.io/">fio Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://coredns.io/manual/toc/">CoreDNS</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.github.io/ingress-nginx/">NGINX Ingress Controller</a></p>
</li>
</ul>
<hr />
<p>In the next post, we start with the heart of the cluster: <code>etcd</code> component → <a target="_blank" href="https://blog.nh4ttruong.me/benchmarking-etcd-the-heartbeat-of-kubernetes">Kubernetes etcd Performance Benchmarks</a></p>
<div class="hn-embed-widget" id="nh4ttruong"></div>]]></content:encoded></item><item><title><![CDATA[Setting Up SAML Single Sign-On in Jira with Keycloak IDP]]></title><description><![CDATA[In the latest Jira products, including Jira Software and Jira Service Management, users can configure their own SAML/OAuth2 Identity Provider (IDP) without needing any plugins or extensions. This guide will help you configure SAML in your Jira applic...]]></description><link>https://blog.nh4ttruong.me/setting-up-saml-single-sign-on-in-jira-with-keycloak-idp</link><guid isPermaLink="true">https://blog.nh4ttruong.me/setting-up-saml-single-sign-on-in-jira-with-keycloak-idp</guid><category><![CDATA[JIRA]]></category><category><![CDATA[keycloak]]></category><category><![CDATA[SSO]]></category><category><![CDATA[SAML]]></category><category><![CDATA[guide]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Wed, 15 Oct 2025 04:47:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760503469180/07fce432-76c2-4680-9937-750462cd0973.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the latest Jira products, including Jira Software and Jira Service Management, users can configure their own SAML/OAuth2 Identity Provider (IDP) without needing any plugins or extensions. This guide will help you configure SAML in your Jira application using Keycloak as the SAML IDP.</p>
<h2 id="heading-1-create-keycloak-saml-client-as-identity-provider">1. Create Keycloak SAML client as Identity Provider</h2>
<ul>
<li><p>Log in to your Keycloak account, select your realm to create your client for Jira authentication</p>
</li>
<li><p>Navigate <strong>Client</strong> → <strong>Create client</strong></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760439553017/b5f09bcb-27cc-495b-859e-9c8ed3e5d52c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Configure the client with the following required values:</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Key</strong></td><td><strong>Value</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Client ID</td><td><code>https://{jira_host}</code></td></tr>
<tr>
<td>Root URL</td><td><code>https://{jira_host}/</code></td></tr>
<tr>
<td>Home URL</td><td><code>https://{jira_host}/</code></td></tr>
<tr>
<td>Valid redirect URIs</td><td><code>https://{jira_host}/*</code></td></tr>
<tr>
<td>IDP-Initiated SSO URL name</td><td><code>https://{keycloak_host}/realms/master/protocol/saml</code></td></tr>
<tr>
<td>Master SAML Processing URL</td><td><code>https://{jira_host}/plugins/servlet/samlconsumer</code></td></tr>
<tr>
<td>Name ID format</td><td>email</td></tr>
<tr>
<td>Force name ID format</td><td>On</td></tr>
<tr>
<td>Force POST binding</td><td>On</td></tr>
<tr>
<td>Include AuthnStatement</td><td>On</td></tr>
<tr>
<td>Sign documents</td><td>On</td></tr>
<tr>
<td>Sign assertions</td><td>On</td></tr>
</tbody>
</table>
</div><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440065269/1e4dac55-d0b9-4614-901a-dcb3e43b86ca.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Switch to <strong>Key</strong> tab, turn off <strong>Signing keys config</strong>:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440093157/97869bdb-065d-4499-b623-ea24e53ba1ec.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Switch to <strong>Client scopes</strong> tab:</p>
<ul>
<li><p>Change <strong>role_list</strong> to <strong>Optional</strong> (if your client had). It will prevent <a target="_blank" href="https://support.atlassian.com/jira/kb/found-an-attribute-element-with-duplicated-name-error-while-users-tries-to-login-using-sso/"><strong>Attribute element with duplicated Name error</strong></a></p>
</li>
<li><p>Choose the dedicated <strong>Assigned client scope</strong> to add new mappers:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440507009/34507dce-bf7e-4b21-b70a-56ace2ef3674.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</li>
<li><p>Next, config <strong>Group list</strong> and <strong>User Property</strong>:</p>
<ul>
<li><p>Configure a new mapper:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440466981/f8d6e1b5-ac9f-43d4-8f32-65693bbca481.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Add <code>memberOf</code> as <strong>Group list</strong> to allow Jira to get your member groups:</p>
</li>
<li><p>Add <code>firstName</code> , <code>lastName</code> , <code>email</code> as <strong>User Property</strong> to allow Jira to get users information</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>        <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440586319/36a33ec9-37f4-4bc8-8676-05f22fc3d98f.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Switch to <strong>Advanced</strong> tab, config <code>https://{jira_host}/plugins/servlet/samlconsumer</code> as <strong>Assertion Consumer Service POST Binding URL</strong> and <strong>Logout Service POST Binding URL:</strong></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440661863/2371d585-7ea3-452b-a415-f3db525226dd.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Navigate <strong>Realms Setting</strong>, choose <strong>Key</strong> tab, see <strong>RS256</strong> and copy and remember the <strong>Certificate</strong> for next step:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760440776056/3ad58310-844b-460f-909b-17d50d398daa.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<hr />
<h1 id="heading-2-config-jira-authentication-method">2. Config Jira authentication method</h1>
<p>The latest Jira products support authentication methods that allow authentication via SAML/OAuth2:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760499579218/e61a4afd-11a8-4e9f-ae56-aed5507197f2.png" alt class="image--center mx-auto" /></p>
<p>Now, let's configure SAML as Single Sign-On in Jira. I will use Jira Service Management as an example:</p>
<ol>
<li><p>Access <strong>https://{jira_host}/plugins/servlet/authentication-config</strong> (change your DNS) or click the top right <strong>gear icon</strong>, choose <strong>System</strong>, navigate <strong>Authentication methods</strong> in left navbar, choose <strong>Add configuration</strong>. Fill the Name and choose <strong>SAML</strong> as <strong>Authentication method</strong></p>
</li>
<li><p>Next fill the <strong>Name</strong> and required options for <strong>SAML SSO settings</strong> as table below:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760499956130/2ffc4891-56f6-42ce-8c34-dd24f41eaa66.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Key</strong></td><td><strong>Value</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Single sign-on issuer</td><td>https://{keycloak_host}/realms/{realms}</td></tr>
<tr>
<td>Identity provider single sign-on URL</td><td>https://{keycloak_host}/realms/{realms}/protocol/saml</td></tr>
<tr>
<td>X.509 Certificate</td><td>Paste certificate which obtain in previous step</td></tr>
<tr>
<td>Username mapping</td><td>${NameID}</td></tr>
<tr>
<td>Name ID Policy</td><td>Email Address</td></tr>
<tr>
<td>Sign requests</td><td>Off/Uncheck</td></tr>
</tbody>
</table>
</div><ol start="3">
<li>If you want <a target="_blank" href="https://confluence.atlassian.com/enterprise/just-in-time-user-provisioning-1005342571.html">JIT</a> to allow users to be created and updated automatically when they log in through SSO to Atlassian Data Center applications, specify as below:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760500359416/324d199a-645b-4a0d-82c1-7f403ae94264.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li><p>Config remain options and click <strong>Save configuration</strong> to finish:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760500431631/666dd1f0-fede-4f7d-9e6a-28bb6866fdb3.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Back to Authentication methods, we have configured SAML:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760500531650/fd758494-3119-4a0c-8ffd-18c32146b3bf.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Click <strong>Action</strong>, <strong>Test sign-in</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760500529328/683ef1b1-8d35-4aaf-ac71-4168dd33af9a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Login with your SAML account, and then we accessed Jira:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760500634420/c2962fc0-c11d-4379-a046-8f11ce327380.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<hr />
<p>You have successfully configured SAML authentication for Jira Service Management using Keycloak as the Identity Provider. This setup will streamline the login process for your Jira applications, making it easier for users to access them.</p>
]]></content:encoded></item><item><title><![CDATA[Guide to Access S3 Storage as Local Filesystem]]></title><description><![CDATA[S3 object storage offers scalable and cost-effective storage solutions but working with it directly can be challenging when your applications expect traditional filesystem access. This guide explores two powerful tools - rclone and s3fs - that bridge...]]></description><link>https://blog.nh4ttruong.me/guide-to-access-s3-storage-as-local-filesystem</link><guid isPermaLink="true">https://blog.nh4ttruong.me/guide-to-access-s3-storage-as-local-filesystem</guid><category><![CDATA[S3]]></category><category><![CDATA[filesystem]]></category><category><![CDATA[clone]]></category><category><![CDATA[s3fs]]></category><category><![CDATA[Devops]]></category><category><![CDATA[system]]></category><category><![CDATA[fuse]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Thu, 10 Jul 2025 17:00:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752116445754/42b78cd9-c094-4d5d-ba1e-5bad0fb5c5e4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>S3 object storage offers scalable and cost-effective storage solutions but working with it directly can be challenging when your applications expect traditional filesystem access. This guide explores two powerful tools - <code>rclone</code> and <code>s3fs</code> - that bridge this gap by mounting S3 buckets as local filesystems.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before getting started, ensure you have installed the required third-party software:</p>
<ul>
<li><p><code>rclone</code>: A versatile command-line tool for managing files on cloud storage</p>
<ul>
<li><p>Installation: <a target="_blank" href="https://rclone.org/install/">https://rclone.org/install/</a></p>
</li>
<li><p>Supports numerous storage providers beyond S3</p>
</li>
</ul>
</li>
<li><p><code>s3fs</code>: A FUSE-based filesystem specifically designed for S3</p>
<ul>
<li><p>Installation: <a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse">https://github.com/s3fs-fuse/s3fs-fuse</a></p>
</li>
<li><p>Available in most package managers: <code>apt install s3fs</code> or <code>yum install s3fs-fuse</code></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-configuring-your-s3-mount-tools">Configuring Your S3 Mount Tools</h2>
<h3 id="heading-step-1-setting-up-configuration-files">Step 1: Setting Up Configuration Files</h3>
<p>Each tool requires specific configuration to connect to your S3 bucket:</p>
<h4 id="heading-rclone-configuration">rclone Configuration</h4>
<p>Create a configuration file at <code>/etc/rclone.conf</code>:</p>
<pre><code class="lang-ini"><span class="hljs-section">[s3-mount]</span>
<span class="hljs-attr">type</span> = s3
<span class="hljs-attr">provider</span> = AWS
<span class="hljs-attr">env_auth</span> = <span class="hljs-literal">false</span>
<span class="hljs-attr">access_key_id</span> = YOUR_ACCESS_KEY
<span class="hljs-attr">secret_access_key</span> = YOUR_SECRET_KEY
<span class="hljs-attr">endpoint</span> = YOUR_ENDPOINT_URL
<span class="hljs-attr">acl</span> = private
</code></pre>
<p>See <a target="_blank" href="rclone/rclone.config.example">rclone.config.example</a> for a complete template.</p>
<h4 id="heading-s3fs-configuration">s3fs Configuration</h4>
<p>Create a credentials file at <code>/etc/passwd-s3fs</code> with the following format:</p>
<pre><code class="lang-python">ACCESS_KEY_ID:SECRET_ACCESS_KEY
</code></pre>
<p>Set appropriate permissions:</p>
<pre><code class="lang-bash">chmod 600 /etc/passwd-s3fs
</code></pre>
<p>See <a target="_blank" href="s3fs/s3fs-passwd.example">s3fs-passwd.example</a> for reference.</p>
<h3 id="heading-step-2-creating-mount-scripts">Step 2: Creating Mount Scripts</h3>
<p>Create shell scripts to manage the mounting process with proper parameters:</p>
<h4 id="heading-rclone-mount-script">rclone Mount Script</h4>
<p>Create <code>/usr/local/bin/rclone-mount.sh</code>:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># Configuration variables</span>
bucket=<span class="hljs-string">"your-bucket-name"</span>
url=<span class="hljs-string">"https://your-endpoint.com"</span>
mount_point=<span class="hljs-string">"/mnt/s3-bucket"</span>
config_file=<span class="hljs-string">"/etc/rclone.conf"</span>
log_file=<span class="hljs-string">"/var/log/rclone-mount.log"</span>
log_level=<span class="hljs-string">"DEBUG"</span>
provider=<span class="hljs-string">"s3"</span>  <span class="hljs-comment"># Options: vstorage, s3, etc.</span>

<span class="hljs-comment"># Create mount point if it doesn't exist</span>
mkdir -p <span class="hljs-string">"<span class="hljs-variable">${mount_point}</span>"</span>

<span class="hljs-comment"># Mount the bucket</span>
rclone mount \
  --config <span class="hljs-string">"<span class="hljs-variable">${config_file}</span>"</span> \
  --log-file <span class="hljs-string">"<span class="hljs-variable">${log_file}</span>"</span> \
  --log-level <span class="hljs-string">"<span class="hljs-variable">${log_level}</span>"</span> \
  --allow-other \
  --file-perms 0644 \
  --dir-perms 0755 \
  --vfs-cache-mode full \
  --vfs-cache-max-size 1G \
  --vfs-read-chunk-size 10M \
  --daemon \
  <span class="hljs-string">"<span class="hljs-variable">${provider}</span>:<span class="hljs-variable">${bucket}</span>"</span> <span class="hljs-string">"<span class="hljs-variable">${mount_point}</span>"</span>

<span class="hljs-built_in">exit</span> 0
</code></pre>
<p>Make the script executable:</p>
<pre><code class="lang-bash">chmod +x /usr/<span class="hljs-built_in">local</span>/bin/rclone-mount.sh
</code></pre>
<h4 id="heading-s3fs-mount-script">s3fs Mount Script</h4>
<p>Create <code>/usr/local/bin/s3fs-mount.sh</code>:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># Configuration variables</span>
bucket=<span class="hljs-string">"your-bucket-name"</span>
url=<span class="hljs-string">"https://your-endpoint.com"</span>
mount_point=<span class="hljs-string">"/mnt/s3-bucket"</span>
passwd_file=<span class="hljs-string">"/etc/passwd-s3fs"</span>
log_file=<span class="hljs-string">"/var/log/s3fs-mount.log"</span>
log_level=<span class="hljs-string">"debug"</span>
region=<span class="hljs-string">"HCM03"</span>  <span class="hljs-comment"># Your specific region</span>

<span class="hljs-comment"># Create mount point if it doesn't exist</span>
mkdir -p <span class="hljs-string">"<span class="hljs-variable">${mount_point}</span>"</span>

<span class="hljs-comment"># Mount the bucket</span>
s3fs <span class="hljs-string">"<span class="hljs-variable">${bucket}</span>"</span> <span class="hljs-string">"<span class="hljs-variable">${mount_point}</span>"</span> \
  -o passwd_file=<span class="hljs-string">"<span class="hljs-variable">${passwd_file}</span>"</span> \
  -o url=<span class="hljs-string">"<span class="hljs-variable">${url}</span>"</span> \
  -o use_path_request_style \
  -o allow_other \
  -o <span class="hljs-built_in">umask</span>=0022 \
  -o dbglevel=<span class="hljs-string">"<span class="hljs-variable">${log_level}</span>"</span> \
  -o curldbg \
  -o endpoint=<span class="hljs-string">"<span class="hljs-variable">${region}</span>"</span> \
  &gt; <span class="hljs-string">"<span class="hljs-variable">${log_file}</span>"</span> 2&gt;&amp;1

<span class="hljs-built_in">exit</span> 0
</code></pre>
<p>Make the script executable:</p>
<pre><code class="lang-bash">chmod +x /usr/<span class="hljs-built_in">local</span>/bin/s3fs-mount.sh
</code></pre>
<h3 id="heading-step-3-creating-systemd-service-units">Step 3: Creating Systemd Service Units</h3>
<p>To ensure your S3 bucket mounts automatically at boot and is properly managed by systemd:</p>
<h4 id="heading-rclone-systemd-service">rclone Systemd Service</h4>
<p>Create <code>/lib/systemd/system/rclone-mount.service</code>:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Mount S3 Bucket using rclone
<span class="hljs-attr">After</span>=network-<span class="hljs-literal">on</span>line.target
<span class="hljs-attr">Wants</span>=network-<span class="hljs-literal">on</span>line.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=forking
<span class="hljs-attr">ExecStart</span>=/usr/local/bin/rclone-mount.sh
<span class="hljs-attr">Restart</span>=<span class="hljs-literal">on</span>-failure
<span class="hljs-attr">RestartSec</span>=<span class="hljs-number">10</span>

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<h4 id="heading-s3fs-systemd-service">s3fs Systemd Service</h4>
<p>Create <code>/lib/systemd/system/s3fs-mount.service</code>:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Mount S3 Bucket using s3fs
<span class="hljs-attr">After</span>=network-<span class="hljs-literal">on</span>line.target
<span class="hljs-attr">Wants</span>=network-<span class="hljs-literal">on</span>line.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=<span class="hljs-literal">on</span>eshot
<span class="hljs-attr">ExecStart</span>=/usr/local/bin/s3fs-mount.sh
<span class="hljs-attr">RemainAfterExit</span>=<span class="hljs-literal">yes</span>
<span class="hljs-attr">ExecStop</span>=/bin/fusermount -u /mnt/s3-bucket

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<h3 id="heading-step-4-enable-and-start-the-service">Step 4: Enable and Start the Service</h3>
<p>Choose which tool you prefer (rclone or s3fs) and enable its service:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># For rclone</span>
sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> rclone-mount.service --now
sudo systemctl status rclone-mount.service

<span class="hljs-comment"># For s3fs</span>
sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> s3fs-mount.service --now
sudo systemctl status s3fs-mount.service
</code></pre>
<h2 id="heading-performance-considerations">Performance Considerations</h2>
<ul>
<li><p><strong>rclone</strong>:</p>
<ul>
<li><p>Offers better performance for large files</p>
</li>
<li><p>More feature-rich with built-in caching</p>
</li>
<li><p>Uses more memory but provides better throughput</p>
</li>
<li><p>Excellent for backup/sync operations</p>
</li>
</ul>
</li>
<li><p><strong>s3fs</strong>:</p>
<ul>
<li><p>Simpler, lighter resource footprint</p>
</li>
<li><p>Better for direct file access patterns</p>
</li>
<li><p>More POSIX-compliant but slower for metadata operations</p>
</li>
<li><p>Good for applications that need basic file access</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<h3 id="heading-mount-failure">Mount Failure</h3>
<p>If your mount fails to initialize:</p>
<ol>
<li><p><strong>Check credentials</strong>: Verify your access keys are correct in the configuration files</p>
<pre><code class="lang-bash"> cat /var/<span class="hljs-built_in">log</span>/rclone-mount.log | grep <span class="hljs-string">"auth"</span>
 <span class="hljs-comment"># or</span>
 cat /var/<span class="hljs-built_in">log</span>/s3fs-mount.log | grep <span class="hljs-string">"auth"</span>
</code></pre>
</li>
<li><p><strong>Test connectivity</strong>: Confirm network access to your S3 endpoint</p>
<pre><code class="lang-bash"> curl -I https://your-endpoint.com
</code></pre>
</li>
<li><p><strong>Permissions</strong>: Ensure your mount scripts are executable</p>
<pre><code class="lang-bash"> ls -la /usr/<span class="hljs-built_in">local</span>/bin/rclone-mount.sh
 ls -la /usr/<span class="hljs-built_in">local</span>/bin/s3fs-mount.sh
</code></pre>
</li>
<li><p><strong>Bucket existence</strong>: Verify the bucket name is spelled correctly and exists</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># For AWS S3</span>
 aws s3 ls s3://your-bucket-name

 <span class="hljs-comment"># For other S3 providers, use their CLI tools</span>
</code></pre>
</li>
</ol>
<h3 id="heading-performance-issues">Performance Issues</h3>
<p>If you experience slow access:</p>
<ol>
<li><p><strong>Increase cache size</strong>: For rclone, modify the <code>--vfs-cache-max-size</code> parameter</p>
</li>
<li><p><strong>Adjust chunk size</strong>: Modify <code>--vfs-read-chunk-size</code> for your workload</p>
</li>
<li><p><strong>Check network latency</strong>: High latency to your S3 endpoint will impact performance</p>
</li>
<li><p><strong>Consider local caching</strong>: For frequently accessed files</p>
</li>
</ol>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://rclone.org/docs/">rclone Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://rclone.org/s3/">rclone S3 Configuration</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse">s3fs Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://www.kernel.org/doc/html/latest/filesystems/fuse.html">FUSE Filesystem Overview</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/s3/">AWS S3 Documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Root Me Solutions & Write-ups]]></title><description><![CDATA[This repository offers write-ups and solutions for Root Me CTF challenges, aimed at educational and ethical hacking practice. It provides step-by-step guides for various web security, application security, and digital forensics challenges.

TL;DR


⚠...]]></description><link>https://blog.nh4ttruong.me/root-me-solutions-and-write-ups</link><guid isPermaLink="true">https://blog.nh4ttruong.me/root-me-solutions-and-write-ups</guid><category><![CDATA[Write Up]]></category><category><![CDATA[root-me]]></category><category><![CDATA[Solution]]></category><category><![CDATA[CTF]]></category><category><![CDATA[CTF Writeup]]></category><category><![CDATA[rootme]]></category><category><![CDATA[Writeup]]></category><category><![CDATA[#capturetheflag]]></category><category><![CDATA[education]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Wed, 09 Jul 2025 17:00:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752045550879/1968d1e5-bd1c-4b73-9855-5d0d44e765fb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This repository offers write-ups and solutions for <a target="_blank" href="https://nh4ttruong.github.io/emtoor">Root Me CTF challenges</a>, aimed at educational and ethical hacking practice. It provides step-by-step guides for various web security, application security, and digital forensics challenges.</p>
<blockquote>
<p><a target="_blank" href="https://nh4ttruong.github.io/emtoor/"><strong>TL;DR</strong></a></p>
</blockquote>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Disclaimer: Please read the <a target="_self" href="https://nh4ttruong.github.io/emtoor/disclaimer.html">Disclaimer</a> before "dive" into the challenges.</div>
</div>

<hr />
<h2 id="heading-categories">Categories</h2>
<p>This repository is organized into several categories, each focusing on different aspects of cybersecurity challenges:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Category</strong></td><td><strong>Description</strong></td><td><strong>Quick Access</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Cross-Site Scripting</td><td>XSS attack techniques &amp; solutions</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/Cross-Site-Scripting/index.html">XSS challenges</a></td></tr>
<tr>
<td>CSRF</td><td>Cross-Site Request Forgery</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/CSRF/index.html">CSRF challenges</a></td></tr>
<tr>
<td>PHP Vulnerabilities</td><td>File inclusion, upload, etc.</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/PHP/index.html">PHP challenges</a></td></tr>
<tr>
<td>SQL Injection</td><td>SQLi types and bypasses</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/SQL-Injection/index.html">SQL Injection challenges</a></td></tr>
<tr>
<td>Steganography</td><td>Hidden data in files/images</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/Steganography/index.html">Steganography challenges</a></td></tr>
<tr>
<td>Forensics</td><td>Digital forensics challenges</td><td><a target="_blank" href="https://nh4ttruong.github.io/emtoor/Forensics/index.html">Forensics challenges</a></td></tr>
</tbody>
</table>
</div><div data-node-type="callout">
<div data-node-type="callout-emoji">🗒</div>
<div data-node-type="callout-text">Note: This is just my solution approach, and it may not be correct at the time you attempt the challenge.</div>
</div>

<hr />
<h2 id="heading-contributing">Contributing</h2>
<p>Contributions, corrections, and new write-ups are welcome! Please open an issue or pull request.</p>
]]></content:encoded></item><item><title><![CDATA[Handling Zalo OA API with Python wrapper]]></title><description><![CDATA[A simple API wrapper script serves as a straightforward API wrapper for the Zalo Official Account (OA), offering a user-friendly interface to efficiently manage users, access detailed user information, and facilitate message exchanges. It's perfect f...]]></description><link>https://blog.nh4ttruong.me/handling-zalo-oa-api-with-python-wrapper</link><guid isPermaLink="true">https://blog.nh4ttruong.me/handling-zalo-oa-api-with-python-wrapper</guid><category><![CDATA[zalo-oa]]></category><category><![CDATA[zalo]]></category><category><![CDATA[Python]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Wed, 09 Jul 2025 02:53:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752029514248/e90aefad-dec0-4e27-a05f-c82e16c94df1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A simple API wrapper script serves as a straightforward API wrapper for the <a target="_blank" href="https://oa.zalo.me/"><strong>Zalo Official Account (OA)</strong></a>, offering a user-friendly interface to efficiently manage users, access detailed user information, and facilitate message exchanges. It's perfect for automating your <strong>Zalo OA</strong> interactions and enhancing customer engagement.</p>
<h2 id="heading-key-features">Key Features</h2>
<ul>
<li><p><strong>✅ User Management</strong> — Retrieve the full list of users who’ve interacted with your OA.</p>
</li>
<li><p><strong>📇 User Info</strong> — Fetch comprehensive details about individual users.</p>
</li>
<li><p><strong>✉️ Messaging</strong> — Support for sending both text and image messages.</p>
</li>
<li><p><strong>📬 Message Retrieval</strong> — Pull inbound messages from your OA</p>
</li>
</ul>
<h2 id="heading-setup-amp-installation">Setup &amp; Installation</h2>
<ol>
<li>Clone the repository:</li>
</ol>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/nh4ttruong/zalo-oa-api-wrapper.git
<span class="hljs-built_in">cd</span> zalo-oa-api
</code></pre>
<ol start="2">
<li>Install Dependencies: Ensure you have Python 3.7+ and install required packages</li>
</ol>
<pre><code class="lang-bash">pip install -r requirements.txt
</code></pre>
<h2 id="heading-configuration">Configuration</h2>
<ol>
<li>Obtain your <strong>Zalo OA Access Token</strong></li>
</ol>
<ul>
<li><p>Head to the <a target="_blank" href="https://developers.zalo.me/tools/explorer">Zalo API Explorer</a></p>
</li>
<li><p>Choose <strong>OA Access Token</strong> and click <strong>Get Access Token</strong></p>
</li>
<li><p>Tick to allow Term of User and copy the generated <strong>Access Token</strong></p>
</li>
</ul>
<ol start="2">
<li>Create your <code>.env</code> file</li>
</ol>
<pre><code class="lang-bash">ZALO_OA_ZALO_OA_ACCESS_TOKEN=your_token_here
</code></pre>
<ol start="3">
<li><p>Configure messaging behavior. In <code>.env</code>, set flags:</p>
<ul>
<li><p><code>SEND_MESSAGE_TEXT</code>, <code>SEND_MESSAGE_WITH_IMAGE</code></p>
</li>
<li><p><code>SEND_ALL_USERS</code>, <code>SEND_USER_LIST</code></p>
</li>
<li><p><code>IMAGE_FILE_PATH</code>, <code>MESSAGE_CONTENT</code>, or <code>MESSAGE_FILE_PATH</code></p>
</li>
</ul>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">See the <a target="_self" href="https://developers.zalo.me/docs/official-account/bat-dau/kham-pha">API Reference</a> and Examples for details.</div>
</div>

<h2 id="heading-examples">Examples</h2>
<h3 id="heading-sending-a-text-message-to-a-specific-user">Sending a text message to a specific user</h3>
<p>To send a message to a specific user (e.g., <code>user_id = "7186086631826132217"</code>):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> dependencies.messages <span class="hljs-keyword">import</span> *

<span class="hljs-comment"># User ID of the recipient</span>
user_id = <span class="hljs-string">"7186086631826132217"</span>
message_text = <span class="hljs-string">"Hello, this is a test message"</span>

<span class="hljs-comment"># Send a text message to the specified user</span>
send_text_message(ZALO_OA_ACCESS_TOKEN, user_id, message_text)
</code></pre>
<h3 id="heading-sending-an-image-message">Sending an image message</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> dependencies.messages <span class="hljs-keyword">import</span> *
<span class="hljs-keyword">from</span> dependencies.upload <span class="hljs-keyword">import</span> *

<span class="hljs-comment"># Specify the user ID(s) and message content</span>
user_id = <span class="hljs-string">"7186086631826132217"</span>
users = [{<span class="hljs-string">"user_id"</span>: user} <span class="hljs-keyword">for</span> user <span class="hljs-keyword">in</span> user_id.split(<span class="hljs-string">","</span>)]
message_text = <span class="hljs-string">"Hello, here’s an image for you!"</span>

<span class="hljs-comment"># Upload the image and retrieve the attachment ID</span>
attachment_id = upload_media(ZALO_OA_ACCESS_TOKEN, file_path=IMAGE_FILE_PATH, type=<span class="hljs-string">"image"</span>)

<span class="hljs-comment"># Send the message along with the image</span>
send_message_to_users(ZALO_OA_ACCESS_TOKEN, users, message_text=message_text, image_file=IMAGE_FILE_PATH)
</code></pre>
<h3 id="heading-broadcast-messages-to-all-users">Broadcast messages to all users</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> dependencies.messages <span class="hljs-keyword">import</span> *
<span class="hljs-keyword">from</span> dependencies.upload <span class="hljs-keyword">import</span> *
<span class="hljs-keyword">from</span> dependencies.users <span class="hljs-keyword">import</span> *

<span class="hljs-comment"># Check if all users should receive the message</span>
<span class="hljs-keyword">if</span> SEND_ALL_USERS == <span class="hljs-string">'True'</span>:
    users = get_all_users(ZALO_OA_ACCESS_TOKEN)
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># Use a comma-separated list of user IDs from SEND_USER_LIST in .env file</span>
    users = [{<span class="hljs-string">"user_id"</span>: user.strip()} <span class="hljs-keyword">for</span> user <span class="hljs-keyword">in</span> SEND_USER_LIST.split(<span class="hljs-string">","</span>)]

<span class="hljs-comment"># Send text or image message based on configuration</span>
<span class="hljs-keyword">if</span> SEND_MESSAGE_TEXT == <span class="hljs-string">'True'</span>:
    <span class="hljs-keyword">if</span> SEND_MESSAGE_WITH_IMAGE == <span class="hljs-string">'True'</span>:
        send_message_to_users(ZALO_OA_ACCESS_TOKEN, users, message_text=MESSAGE_CONTENT, image_file=IMAGE_FILE_PATH)
    <span class="hljs-keyword">else</span>:
        send_message_to_users(ZALO_OA_ACCESS_TOKEN, users, message_text=MESSAGE_CONTENT)
<span class="hljs-keyword">elif</span> SEND_MESSAGE_WITH_IMAGE == <span class="hljs-string">'True'</span>:
    send_message_to_users(ZALO_OA_ACCESS_TOKEN, users, message_text=MESSAGE_CONTENT, image_file=IMAGE_FILE_PATH)
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">"No message to send"</span>)
</code></pre>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>This lightweight Python wrapper simplifies interaction with the Zalo OA API—handling user retrieval, message sending (text/image), and message intake with ease. Ideal for building engagement pipelines, bots, or customer support tools.</p>
<div class="hn-embed-widget" id="nh4ttruong"></div>]]></content:encoded></item><item><title><![CDATA[How To Clean ETCD Benchmark Efficiently?]]></title><description><![CDATA[A lightweight CLI tool to scan, detect, and optionally remove benchmark or non-UTF8 keys from your etcd key-value store.
This tool was created as an extension because the official etcd/tools/benchmark does not include a built-in clean command or the ...]]></description><link>https://blog.nh4ttruong.me/how-to-clean-etcd-benchmark-efficiently</link><guid isPermaLink="true">https://blog.nh4ttruong.me/how-to-clean-etcd-benchmark-efficiently</guid><category><![CDATA[etcd]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Benchmark]]></category><category><![CDATA[golang]]></category><category><![CDATA[#howtos]]></category><category><![CDATA[k8s]]></category><category><![CDATA[Performance Testing]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Wed, 02 Jul 2025 06:32:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751437906764/d5d94269-7c5d-4fea-b470-9fc5913f990d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A lightweight CLI tool to <strong>scan</strong>, <strong>detect</strong>, and optionally <strong>remove benchmark or non-UTF8 keys</strong> from your etcd key-value store.</p>
<p>This tool was created as an extension because the official <a target="_blank" href="https://github.com/etcd-io/etcd/blob/main/tools/benchmark/README.md">etcd/tools/benchmark</a> does <strong>not include a</strong> built-in <code>clean</code> command or the ability to directly manage invalid or benchmark keys. By default, the <code>etcd benchmark</code> tool creates a large binary keyspace for testing etcd. Therefore, <a target="_blank" href="https://github.com/nh4ttruong/etcd-benchmark-cleaner"><code>etcd-benchmark-cleaner</code></a> helps retrieve and remove unnecessary binary keys in <code>etcd</code>, reducing its size.</p>
<blockquote>
<p>Be cautious and do not run the tool if you are not sure what it does.</p>
</blockquote>
<hr />
<h2 id="heading-features">Features</h2>
<ul>
<li><p>Scan keys under a specified <strong>hex-encoded prefix</strong></p>
</li>
<li><p>Detect benchmark or invalid UTF-8 keys</p>
</li>
<li><p>Supports <strong>dry-run mode</strong> for safe validation</p>
</li>
<li><p>Secure connection via TLS</p>
</li>
<li><p>Clear, color-coded terminal output for easy inspection</p>
</li>
</ul>
<hr />
<h2 id="heading-installation">🔧 Installation</h2>
<p>Install package:</p>
<pre><code class="lang-bash">go install github.com/nh4ttruong/etcd-benchmark-cleaner@latest
<span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">${PATH}</span>:`go env GOPATH`/bin &amp;&amp; <span class="hljs-built_in">which</span> etcd-benchmark-cleaner
</code></pre>
<p>Or manual build locally:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/nh4ttruong/etcd-benchmark-cleaner.git
<span class="hljs-built_in">cd</span> etcd-benchmark-cleaner
go build -o etcd-benchmark-cleaner
<span class="hljs-comment"># Or run directly with `go run clean.go [flags]`</span>
</code></pre>
<hr />
<h2 id="heading-usage">Usage</h2>
<pre><code class="lang-bash">./etcd-benchmark-cleaner [flags]
    Usage of etcd-benchmark-cleaner:
        --cacert string
                Path to trusted CA file (default <span class="hljs-variable">$ETCDCTL_CACERT</span>)
        --cert string
                Path to client certificate (default <span class="hljs-variable">$ETCDCTL_CERT</span>)
        --debug
                Print UTF-8 keys and values
        --dry
                Dry-run mode (simulates deletion)
        --endpoints string
                Comma-separated list of etcd endpoints (default <span class="hljs-variable">$ETCDCTL_ENDPOINTS</span>)
        --key string
                Path to client private key (default <span class="hljs-variable">$ETCDCTL_KEY</span>)
        --prefix string
                Hexadecimal prefix of keys to scan
        --remove
                Delete binary keys
        --timeout duration
                Request timeout (default 5s)
</code></pre>
<p>Flags to run <code>etcd-benchmark-cleaner</code>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Flag</strong></td><td><strong>Default</strong></td><td><strong>Description</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>--endpoints</code></td><td><a target="_blank" href="http://localhost:2379">localhost:2379</a></td><td>Comma-separated list of etcd endpoints <strong>(required)</strong></td></tr>
<tr>
<td><code>--prefix</code></td><td>"" (all)</td><td><strong>Hex-encoded</strong> prefix of keys to scan (e.g., <code>02</code>, <code>74657374</code>)</td></tr>
<tr>
<td><code>--cacert</code></td><td><code>$ETCDCTL_CACERT</code></td><td>Path to CA file (or set <code>$ETCDCTL_CACERT</code>)</td></tr>
<tr>
<td><code>--cert</code></td><td><code>$ETCDCTL_CERT</code></td><td>Path to client cert (or set <code>$ETCDCTL_CERT</code>)</td></tr>
<tr>
<td><code>--key</code></td><td><code>$ETCDCTL_KEY</code></td><td>Path to client key (or set <code>$ETCDCTL_KEY</code>)</td></tr>
<tr>
<td><code>--debug</code></td><td><em>N/A</em></td><td>Print raw UTF-8 keys and values</td></tr>
<tr>
<td><code>--dry</code></td><td><em>N/A</em></td><td>Simulate deletion without making changes</td></tr>
<tr>
<td><code>--remove</code></td><td><em>N/A</em></td><td>Remove binary benchmark keys (caution)</td></tr>
<tr>
<td><code>--timeout</code></td><td>5s</td><td>Request timeout (default: <code>5s</code>)</td></tr>
</tbody>
</table>
</div><h2 id="heading-examples">Examples</h2>
<h3 id="heading-scan-all-keys-for-benchmark-entries">Scan all keys for benchmark entries</h3>
<pre><code class="lang-bash">./etcd-benchmark-cleaner --endpoints=https://127.0.0.1:2379
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751436258788/e40ea6ec-9166-44d0-a841-107d04fbb9dd.png" alt /></p>
<h3 id="heading-scan-keys-with-a-benchmark-prefix-0x00">Scan keys with a benchmark prefix (<code>0x00</code>)</h3>
<pre><code class="lang-bash">./etcd-benchmark-cleaner --endpoints=https://127.0.0.1:2379 --prefix 00
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751436280016/7c404448-8290-4ce0-9c3e-82b48f1bd2f7.png" alt /></p>
<h3 id="heading-dry-run-deletion-of-benchmark-keys-no-changes-made">Dry-run deletion of benchmark keys (no changes made)</h3>
<pre><code class="lang-bash">./etcd-benchmark-cleaner --endpoints=https://127.0.0.1:2379 --prefix 02 --dry
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751436287721/2bcc9937-68af-45cf-a46b-9bba8c44a68a.png" alt /></p>
<h3 id="heading-remove-binary-benchmark-keys-irreversible">Remove binary benchmark keys (irreversible)</h3>
<pre><code class="lang-bash">./etcd-benchmark-cleaner --endpoints=https://127.0.0.1:2379 --remove
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751436298308/61b0d144-eaaf-4537-8868-16451afcf38c.png" alt /></p>
<hr />
<h2 id="heading-tls-support">🔐 TLS Support</h2>
<p>If your etcd cluster uses TLS, provide the following flags:</p>
<pre><code class="lang-bash">--cacert path/to/ca.crt
--cert   path/to/client.crt
--key    path/to/client.key
</code></pre>
<p>Or set them as environment variables:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> ETCDCTL_CACERT=...
<span class="hljs-built_in">export</span> ETCDCTL_CERT=...
<span class="hljs-built_in">export</span> ETCDCTL_KEY=...
</code></pre>
<hr />
<h2 id="heading-best-practice">Best Practice</h2>
<p>After running <code>etcd-benchmark-cleaner</code>, you should obtain the safe revision of the etcd state, then compact at that revision and defrag each etcd node.</p>
<blockquote>
<p>Please check the further information about <code>compact</code> and <code>defrag</code> at <a target="_blank" href="https://etcd.io/docs/v3.6/op-guide/maintenance/">ETCD | Maintenance guide</a></p>
</blockquote>
<pre><code class="lang-bash"><span class="hljs-comment"># Get safe revision</span>
etcdctl endpoint status --write-out=json | jq <span class="hljs-string">'[.[] | .Status.header.revision]'</span>

<span class="hljs-comment"># Compact using the previous safe revision. Perform this on one of the three etcd nodes.</span>
etcdctl --endpoints=<span class="hljs-string">"<span class="hljs-variable">$ETCD_NODE_1</span>"</span> compact &lt;safe_revison_id&gt;

<span class="hljs-comment"># Defrag nodes in the following order</span>
etcdctl --endpoints=<span class="hljs-string">"<span class="hljs-variable">$ETCD_NODE_1</span>"</span> defrag &amp;&amp; sleep 10
etcdctl --endpoints=<span class="hljs-string">"<span class="hljs-variable">$ETCD_NODE_2</span>"</span> defrag &amp;&amp; sleep 10
etcdctl --endpoints=<span class="hljs-string">"<span class="hljs-variable">$ETCD_NODE_3</span>"</span> defrag

<span class="hljs-comment"># Watch change in etcd DB size</span>
etcdctl endpoint status --write-out=json
</code></pre>
<hr />
<h2 id="heading-note">Note</h2>
<ul>
<li><p>Always run with <code>--dry</code> first before using <code>--remove</code></p>
</li>
<li><p>Ensure your prefix is correct and <strong>hex-encoded</strong></p>
</li>
<li><p>Backup etcd or test against a dev cluster before destructive operations</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[A Guide to Managing Kubernetes Secrets with AWS Secrets Manager and External Secrets Operator]]></title><description><![CDATA[Managing secrets in Kubernetes is notoriously tricky. Hardcoding them? Yikes. Storing them in plaintext? Dangerous. In this post, I’ll show you how to securely integrate AWS Secrets Manager into your K8s workflow using External Secrets Operator (ESO)...]]></description><link>https://blog.nh4ttruong.me/a-guide-to-managing-kubernetes-secrets-with-aws-secrets-manager-and-external-secrets-operator</link><guid isPermaLink="true">https://blog.nh4ttruong.me/a-guide-to-managing-kubernetes-secrets-with-aws-secrets-manager-and-external-secrets-operator</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Devops]]></category><category><![CDATA[AWS]]></category><category><![CDATA[external-secrets]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[cloud security]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Wed, 14 May 2025 07:33:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747207970631/51e44f0f-0ae6-4ce4-97c8-d28f42f1a950.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Managing secrets in Kubernetes is notoriously tricky. Hardcoding them? Yikes. Storing them in plaintext? Dangerous. In this post, I’ll show you how to securely integrate AWS Secrets Manager into your K8s workflow using External Secrets Operator (ESO) - so you can automate secret syncing and sleep better at night.</p>
<p>This post walks you through a clean, secure approach: syncing secrets from <strong>AWS Secrets Manager (SM)</strong> into Kubernetes using the <strong>External Secrets Operator (ESO)</strong>. You'll learn how to set it up with Helm, configure access policies, and sync secrets in different formats - using real examples from the trenches.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:700/1*I_Txf2Bm3H8EJ6mBdHKSow.png" alt="ExternalSecrets architecture - Ali Nadir" class="image--center mx-auto" /></p>
<h1 id="heading-overview">Overview</h1>
<p>ExternalSecrets is an open-source Kubernetes plugin that serves two main functions: it injects secrets from supported external providers into your application cluster and synchronizes these injected secrets with their corresponding remote counterparts.</p>
<p>In the ExternalSecrets architecture, two key resources play crucial roles:</p>
<ol>
<li><p><strong>SecretStore</strong>: This resource manages authentication, enabling your Kubernetes cluster to access AWS resources, specifically secrets. It acts as a bridge, ensuring secure and authorized access to the secrets stored in AWS.</p>
</li>
<li><p><strong>ExternalSecret</strong>: This resource is responsible for defining and creating secrets. It utilizes the SecretStore to retrieve specific secrets and provides a template for Kubernetes controllers to generate local secrets within the cluster.</p>
</li>
</ol>
<h1 id="heading-step-by-step">Step-By-Step</h1>
<h2 id="heading-install-external-secrets-operator-via-helm">🛠️ Install External Secrets Operator via Helm</h2>
<p>First, install the <a target="_blank" href="https://artifacthub.io/packages/helm/external-secrets-operator/external-secrets">ESO Helm chart</a> into its own namespace:</p>
<pre><code class="lang-bash">kubectl create ns eso
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n eso
</code></pre>
<blockquote>
<p>🚫 Optional: If you manage CRDs manually, add <code>--set installCRDs=false</code>.</p>
</blockquote>
<h2 id="heading-aws-iam-setup-for-external-secrets">🔐 AWS IAM Setup for External Secrets</h2>
<p>We need to create an IAM user or role with access to read specific secrets from AWS Secrets Manager.</p>
<p>Example IAM Policy:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
  <span class="hljs-attr">"Statement"</span>: [
    {
      <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
      <span class="hljs-attr">"Action"</span>: [
        <span class="hljs-string">"secretsmanager:ListSecrets"</span>,
        <span class="hljs-string">"secretsmanager:GetSecretValue"</span>,
        <span class="hljs-string">"secretsmanager:ListSecretVersionIds"</span>
      ],
      <span class="hljs-attr">"Resource"</span>: [
        <span class="hljs-string">"arn:aws:secretsmanager:ap-southeast-1:071123451249:secret:demo*"</span>
      ],
      <span class="hljs-attr">"Condition"</span>: {
        <span class="hljs-attr">"StringLike"</span>: {
          <span class="hljs-attr">"secretsmanager:SecretId"</span>: [
            <span class="hljs-string">"arn:aws:secretsmanager:ap-southeast-1:071123451249:secret:prod/demo"</span>
          ]
        },
        <span class="hljs-attr">"StringEquals"</span>: {
          <span class="hljs-attr">"aws:username"</span>: [<span class="hljs-string">"secret-eso"</span>]
        }
      }
    }
  ]
}
</code></pre>
<h3 id="heading-why-this-policy-setup">Why this policy setup?</h3>
<ul>
<li><p><strong>Fine-grained access</strong>: Principle of least privilege.</p>
</li>
<li><p><strong>Conditionals</strong>: Limits access to just the needed secrets + specific IAM username.</p>
</li>
</ul>
<hr />
<h2 id="heading-kubernetes-secret-for-aws-credentials">🔑 Kubernetes Secret for AWS Credentials</h2>
<p>We create a K8s secret that holds AWS access keys.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> -n <span class="hljs-string">'KEYID'</span> &gt; ./access-key
<span class="hljs-built_in">echo</span> -n <span class="hljs-string">'SECRETKEY'</span> &gt; ./secret-access-key

kubectl create secret generic demo-awssm-secret \
  --from-file=./access-key \
  --from-file=./secret-access-key

rm -f ./access-key ./secret-access-key
</code></pre>
<blockquote>
<p>⚠️ Pro tip: Store these secrets in a GitOps-friendly secret manager like SealedSecrets, SOPS, or External Secrets from your Git repo—not directly in plain YAML files.</p>
</blockquote>
<hr />
<h2 id="heading-configuring-the-secretstore">🏗️ Configuring the SecretStore</h2>
<p>This tells ESO how to talk to AWS Secrets Manager:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">external-secrets.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">SecretStore</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secretstore</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">provider:</span>
    <span class="hljs-attr">aws:</span>
      <span class="hljs-attr">service:</span> <span class="hljs-string">SecretsManager</span>
      <span class="hljs-attr">region:</span> <span class="hljs-string">ap-southeast-1</span>
      <span class="hljs-attr">auth:</span>
        <span class="hljs-attr">secretRef:</span>
          <span class="hljs-attr">accessKeyIDSecretRef:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">demo-awssm-secret</span>
            <span class="hljs-attr">key:</span> <span class="hljs-string">access-key</span>
          <span class="hljs-attr">secretAccessKeySecretRef:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">demo-awssm-secret</span>
            <span class="hljs-attr">key:</span> <span class="hljs-string">secret-access-key</span>
</code></pre>
<hr />
<h2 id="heading-syncing-secrets-in-kubernetes">💾 Syncing Secrets in Kubernetes</h2>
<p>You have three common use cases. Here's what each one looks like:</p>
<h3 id="heading-1-configmap-style-secret-plaintext">1. ConfigMap-style Secret (plaintext)</h3>
<p>Best for multi-line config files.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">external-secrets.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ExternalSecret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secret-as-configmap-template</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">refreshInterval:</span> <span class="hljs-string">5m</span>
  <span class="hljs-attr">secretStoreRef:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secretstore</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">SecretStore</span>
  <span class="hljs-attr">target:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-config</span>
    <span class="hljs-attr">template:</span>
      <span class="hljs-attr">engineVersion:</span> <span class="hljs-string">v2</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">core-dev.php:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ .coredev | toString }}</span>"</span>
        <span class="hljs-attr">custom.ini:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ .customini | toString }}</span>"</span>
        <span class="hljs-attr">demo-config.conf:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ .conf| toString }}</span>"</span>
        <span class="hljs-attr">service-url.php:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ .serviceurl | toString }}</span>"</span>
  <span class="hljs-attr">data:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKey:</span> <span class="hljs-string">coredev</span>
      <span class="hljs-attr">remoteRef:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/core-dev.php</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKey:</span> <span class="hljs-string">customini</span>
      <span class="hljs-attr">remoteRef:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/custom.ini</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKey:</span> <span class="hljs-string">conf</span>
      <span class="hljs-attr">remoteRef:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/cnf.conf</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKey:</span> <span class="hljs-string">serviceurl</span>
      <span class="hljs-attr">remoteRef:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/service-url.php</span>
</code></pre>
<blockquote>
<p>🧠 Output: A <code>Secret</code> with multiple keys mimicking a <code>ConfigMap</code>, storing plaintext files.</p>
</blockquote>
<hr />
<h3 id="heading-2-raw-secret-from-json-keyvalue">2. Raw Secret from JSON (key/value)</h3>
<p>Ideal for app credentials or API keys.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">external-secrets.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ExternalSecret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secret</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">refreshInterval:</span> <span class="hljs-string">2m</span>
  <span class="hljs-attr">secretStoreRef:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secretstore</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">SecretStore</span>
  <span class="hljs-attr">target:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secret</span>
    <span class="hljs-attr">creationPolicy:</span> <span class="hljs-string">Owner</span>
  <span class="hljs-attr">dataFrom:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">extract:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/secret</span>
</code></pre>
<blockquote>
<p>🎯 Output: A Kubernetes <code>Secret</code> with key/value pairs extracted from a JSON blob in AWS SM.</p>
</blockquote>
<hr />
<h3 id="heading-3-templated-configmap-redis-config">3. Templated ConfigMap (Redis Config)</h3>
<p>When you want ESO to inject secrets into a full config file template.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">external-secrets.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ExternalSecret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secret-in-configmap</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">refreshInterval:</span> <span class="hljs-string">2m</span>
  <span class="hljs-attr">secretStoreRef:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secretstore</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">SecretStore</span>
  <span class="hljs-attr">target:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">demo-secret-redis-config</span>
    <span class="hljs-attr">template:</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">redis.conf:</span> <span class="hljs-string">|
          bind 0.0.0.0
          port 6379
          requirepass "{{ .redisPassword | toString }}"
          protected-mode no
          appendonly no
          supervised no
          save 3600 1 300 10 30 20
          dir /opt/redis/data
          loglevel notice
          logfile "/opt/redis/data/redis.log"
          databases 6
</span>  <span class="hljs-attr">data:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKey:</span> <span class="hljs-string">redisPassword</span>
      <span class="hljs-attr">remoteRef:</span>
        <span class="hljs-attr">key:</span> <span class="hljs-string">prod/demo/redis-credential</span>
</code></pre>
<blockquote>
<p>⚙️ Output: A fully rendered Redis config with live secrets baked in.</p>
</blockquote>
<hr />
<h1 id="heading-more-info">More Info</h1>
<ul>
<li><p><a target="_blank" href="https://external-secrets.io/v0.16.2/">External Secrets</a></p>
</li>
<li><p><a target="_blank" href="https://artifacthub.io/packages/helm/external-secrets-operator/external-secrets">External Secrest Helm Chart</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">AWS Secrets Manager</a></p>
</li>
<li><p><a target="_blank" href="https://blog.devops.dev/injecting-external-secrets-in-a-kubernetes-cluster-1e9bbe0f0d5b">Injecting AWS Secrets in a Kubernetes Cluster with External Secrets Operator | by Ali Nadir</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[🔐 Managing SharePoint with an Azure App using Sites.Selected Permissions]]></title><description><![CDATA[Use Cases
Why even go through this setup? Here's where this setup totally makes sense:

You want to manage SharePoint without using personal credentials (aka ditch the annoying login prompts).

You need to upload, download, or tweak files in SharePoi...]]></description><link>https://blog.nh4ttruong.me/managing-sharepoint-with-an-azure-app-using-sitesselected-permissions</link><guid isPermaLink="true">https://blog.nh4ttruong.me/managing-sharepoint-with-an-azure-app-using-sitesselected-permissions</guid><category><![CDATA[sysops]]></category><category><![CDATA[Azure]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[Powershell]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Tue, 13 May 2025 06:25:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747117620264/f9bbf0e6-9519-4874-9fd2-576f0f223091.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-use-cases">Use Cases</h2>
<p>Why even go through this setup? Here's where this setup totally makes sense:</p>
<ul>
<li><p>You want to <strong>manage SharePoint without using personal credentials</strong> (aka ditch the annoying login prompts).</p>
</li>
<li><p>You need to <strong>upload, download, or tweak files</strong> in SharePoint from scripts or automated jobs.</p>
</li>
<li><p>You want to build <strong>automation workflows</strong> that don’t ask you to "Sign in with Microsoft" every five minutes.</p>
</li>
</ul>
<blockquote>
<p>🔍 <em>"Manage" means Read, Write, and even changing permissions</em></p>
</blockquote>
<hr />
<h2 id="heading-how-azure-permissions-work">How Azure Permissions Work</h2>
<p>Before we dive in, let’s get our heads around two key types of Azure API permissions:</p>
<ul>
<li><p><strong>Delegated permissions</strong>: These require a signed-in user. Think <strong>"act on behalf of a user"</strong> — good for interactive apps.</p>
</li>
<li><p><strong>Application permissions</strong>: These don’t need a user at all. Perfect for automation and backend stuff. Full freedom with the right consents.</p>
</li>
</ul>
<p>The key player here? <code>Sites.Selected</code> permission. It’s <strong>available in both modes</strong>, but we’re going with <strong>Application</strong> permission because we’re all about that sweet non-interactive automation.</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Here’s what you need to follow along:</p>
<ul>
<li><p>PowerShell with the <a target="_blank" href="https://github.com/pnp/powershell">PnP module</a></p>
</li>
<li><p>An Azure AD Application (we’ll set it up in a sec)</p>
</li>
<li><p>Admin consent for Graph API permissions</p>
</li>
<li><p>Access to the SharePoint site you wanna manage</p>
</li>
</ul>
<hr />
<h2 id="heading-step-by-step">Step-by-Step</h2>
<h3 id="heading-1-register-a-new-azure-app">1. Register a New Azure App</h3>
<p>Go to <strong>Azure Portal</strong> → <strong>App registrations</strong> → <strong>New registration</strong>.</p>
<p><img src="https://vng-docs.s3-accelerate.amazonaws.com/uploads/07dc8c6d-b12e-445e-a77e-fb5e897a46a9/096b2149-fd53-42d4-b40f-1c1581c4478c/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=AKIARBLY6MQN745PDDHH%2F20250513%2Fap-southeast-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20250513T061007Z&amp;X-Amz-Expires=60&amp;X-Amz-Signature=3384465154a15908f8e79e5dbc9f325ad478d283e80681bacc1c699ef226afb5&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject" alt /></p>
<hr />
<h3 id="heading-2-add-permissions">2. Add Permissions</h3>
<ul>
<li><p>Go to your app → <strong>API permissions</strong> → Add:</p>
<ul>
<li><p><code>Sites.FullControl.All</code> (used only temporarily)</p>
</li>
<li><p><code>Sites.Selected</code></p>
</li>
</ul>
</li>
</ul>
<p>🛑 Heads-up: You <em>must</em> request admin consent for these permissions. Microsoft requires <code>Sites.FullControl.All</code> to grant site-specific permissions via <code>Sites.Selected</code>.</p>
<p><img src="https://vng-docs.s3-accelerate.amazonaws.com/uploads/07dc8c6d-b12e-445e-a77e-fb5e897a46a9/52df343a-6fcb-4e4d-8c9d-31617c44a484/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=AKIARBLY6MQN745PDDHH%2F20250513%2Fap-southeast-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20250513T061007Z&amp;X-Amz-Expires=60&amp;X-Amz-Signature=5974e4be610275335968c379e280067d54d6b43b5815ea126d75a0a267984f01&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject" alt /></p>
<hr />
<h3 id="heading-3-create-a-certificate">3. Create a Certificate</h3>
<p>Use PowerShell to generate a cert:</p>
<pre><code class="lang-powershell"><span class="hljs-built_in">New-PnPAzureCertificate</span> <span class="hljs-literal">-OutPfx</span> pnp.pfx <span class="hljs-literal">-OutCert</span> pnp.cer <span class="hljs-literal">-CommonName</span> &lt;tenant&gt;.sharepoint.com
</code></pre>
<p>Now you’ve got <code>pnp.pfx</code> and <code>pnp.cer</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747116693626/83b0376b-c792-4b17-b16b-aaef3c2665c6.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-4-import-certificates">4. Import Certificates</h3>
<p>Browse to the folder, then import both files to <strong>Current User</strong> → just <strong>Next</strong> → <strong>Finish</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747116715746/76b337fc-ef03-41ef-a750-a99bd6fce589.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-5-upload-certificate-to-azure">5. Upload Certificate to Azure</h3>
<p>In the Azure Portal, go back to your app → <strong>Certificates &amp; secrets</strong> → <strong>Certificates tab</strong> → Upload <code>pnp.cer</code></p>
<p>💡 Save the Thumbprint — you'll need it soon.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747116731332/77d79fe3-bea0-4a24-b991-f7282373100e.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-6-connect-to-sharepoint-grant-permissions">6. Connect to SharePoint + Grant Permissions</h3>
<p>Let’s put this all together in PowerShell:</p>
<pre><code class="lang-powershell"><span class="hljs-variable">$siteUrl</span> = <span class="hljs-string">"https://&lt;tenant&gt;.sharepoint.com/sites/&lt;site&gt;"</span>
<span class="hljs-variable">$tenant</span> = <span class="hljs-string">"&lt;tenant&gt;.sharepoint.com"</span>
<span class="hljs-variable">$clientId</span> = <span class="hljs-string">"&lt;Azure Application ID (clientId)&gt;"</span>
<span class="hljs-variable">$certThumbprint</span> = <span class="hljs-string">"&lt;obtain above&gt;"</span>

<span class="hljs-built_in">Connect-PnPOnline</span> <span class="hljs-literal">-Url</span>:<span class="hljs-variable">$siteUrl</span> <span class="hljs-literal">-ClientId</span>:<span class="hljs-variable">$clientId</span> <span class="hljs-literal">-Thumbprint</span>:<span class="hljs-variable">$certThumbprint</span> <span class="hljs-literal">-Tenant</span>:<span class="hljs-variable">$tenant</span>
<span class="hljs-comment"># FullContol/Read/Write/Manage permission https://pnp.github.io/powershell/cmdlets/Grant-PnPAzureADAppSitePermission.html</span>
<span class="hljs-built_in">Grant-PnPAzureADAppSitePermission</span> <span class="hljs-literal">-S</span>ưite <span class="hljs-variable">$siteUrl</span> <span class="hljs-literal">-AppId</span> <span class="hljs-variable">$clientId</span> <span class="hljs-literal">-DisplayName</span> <span class="hljs-string">"SharePoint Permission"</span> <span class="hljs-literal">-Permissions</span> <span class="hljs-string">"Write"</span>

<span class="hljs-comment"># Check</span>
<span class="hljs-built_in">Get-PnPAzureADAppSitePermission</span> <span class="hljs-literal">-Site</span> <span class="hljs-variable">$siteUrl</span> <span class="hljs-literal">-AppIdentity</span> <span class="hljs-variable">$clientId</span>
</code></pre>
<p>Now your app officially has access to that SharePoint site with <code>Sites.Selected</code>.</p>
<hr />
<h3 id="heading-7-clean-up">7. Clean Up</h3>
<p>Once everything works, you can go back and remove <code>Sites.FullControl.All</code> permission. You don’t need it anymore — your app’s now living on like a pro.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Using <code>Sites.Selected</code> with application permissions is low-key one of the best ways to build secure, automated SharePoint workflows — especially when you wanna keep things headless and passwordless.</p>
<p>You get fine-grained control, and you can automate all the boring stuff without needing to run it interactively. SysOps, Power Users, and Scripters — this one's for you.</p>
<hr />
<p>Let me know if you want a follow-up post on automating uploads/downloads using this setup. Happy scripting! 🚀💻</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/graph/permissions-selected-overview?tabs=http#what-permissions-do-i-need-to-manage-permissions">Overview of Selected Permissions in OneDrive and SharePoi</a>nt</p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/graph/permissions-selected-overview?tabs=http#what-permissions-do-i-need-to-manage-permissions">New-PnPAzureCertificate</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/graph/permissions-selected-overview?tabs=http#what-permissions-do-i-need-to-manage-permissions">Grant-PnPAzureADAppSitePermission</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Mastering Disk Usage Analysis With ncdu]]></title><description><![CDATA[As a systems engineer, understanding disk usage is not just beneficial — it's essential for maintaining the health and performance of your servers. In this guide, we’ll explore how to efficiently analyze your filesystem using ncdu along with a few ot...]]></description><link>https://blog.nh4ttruong.me/mastering-disk-usage-analysis-with-ncdu-tool</link><guid isPermaLink="true">https://blog.nh4ttruong.me/mastering-disk-usage-analysis-with-ncdu-tool</guid><category><![CDATA[system]]></category><category><![CDATA[ncdu]]></category><category><![CDATA[disk management]]></category><category><![CDATA[guide]]></category><category><![CDATA[filesystem]]></category><category><![CDATA[sysops]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Mon, 28 Apr 2025 13:59:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745852110826/c23136d8-712f-4f98-b055-75c592fe0cc7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a systems engineer, understanding disk usage is not just beneficial — it's essential for maintaining the health and performance of your servers. In this guide, we’ll explore how to efficiently analyze your filesystem using <code>ncdu</code> along with a few other reliable command-line tools.</p>
<h2 id="heading-what-is-ncdu">What is <code>ncdu</code>?</h2>
<p><code>ncdu</code> (NCurses Disk Usage) is a lightweight, interactive disk usage analyzer.<br />It gives you a fast and easy way to identify which directories and files are hogging your disk space — all through a simple terminal interface.</p>
<h2 id="heading-installing-ncdu">Installing <code>ncdu</code></h2>
<p>Getting <code>ncdu</code> up and running is quick:</p>
<p><strong>On Debian/Ubuntu-based systems:</strong></p>
<pre><code class="lang-bash">sudo apt install ncdu
</code></pre>
<p><strong>On Red Hat/CentOS/Fedora systems:</strong></p>
<pre><code class="lang-bash">sudo yum install ncdu
</code></pre>
<h2 id="heading-using-ncdu-to-analyze-filesystem-usage">Using <code>ncdu</code> to Analyze Filesystem Usage</h2>
<p>Running <code>ncdu</code> is super straightforward — just point it at a directory:</p>
<pre><code class="lang-bash">ncdu /path/to/directory
</code></pre>
<h3 id="heading-restricting-to-a-single-filesystem">Restricting to a Single Filesystem</h3>
<p>One of <code>ncdu</code>'s killer features is the <code>-x</code> option, which restricts the scan to a single filesystem:</p>
<pre><code class="lang-bash">ncdu -x /var/<span class="hljs-built_in">log</span>
</code></pre>
<p>This is really handy when you want to avoid crossing into mounted volumes or other partitions accidentally.</p>
<h3 id="heading-navigating-the-ncdu-interface">Navigating the <code>ncdu</code> Interface</h3>
<p>When you launch <code>ncdu</code>, you can interact with it using your keyboard:</p>
<ul>
<li><p>Arrow keys: Navigate through the directories</p>
</li>
<li><p>Enter: Dive into a directory</p>
</li>
<li><p><code>d</code>: Delete a file or directory (⚠️ use carefully)</p>
</li>
<li><p><code>q</code>: Quit the program</p>
</li>
</ul>
<p>It’s intuitive enough that you’ll be flying through your filesystem in minutes.</p>
<h2 id="heading-alternative-tools-for-disk-usage-analysis">Alternative Tools for Disk Usage Analysis</h2>
<p>While <code>ncdu</code> is amazing for interactive exploration, the good old command line has some other tricks up its sleeve.</p>
<h3 id="heading-1-df-display-free-disk-space">1. <code>df</code> — Display Free Disk Space</h3>
<p>The <code>df</code> command shows you disk space usage and free space across all mounted filesystems:</p>
<pre><code class="lang-bash">df -h
</code></pre>
<p>The <code>-h</code> flag makes it human-readable (i.e., shows sizes in KB, MB, GB).</p>
<p>To focus on a specific directory:</p>
<pre><code class="lang-bash">df -h /var
</code></pre>
<h3 id="heading-2-du-estimate-file-and-directory-sizes">2. <code>du</code> — Estimate File and Directory Sizes</h3>
<p>The <code>du</code> command is perfect for quick checks:</p>
<pre><code class="lang-bash">du -sh /var/<span class="hljs-built_in">log</span>
</code></pre>
<ul>
<li><p><code>-s</code>: Display only the total size</p>
</li>
<li><p><code>-h</code>: Human-readable format</p>
</li>
</ul>
<p>Want a breakdown of subdirectories?</p>
<pre><code class="lang-bash">du -h --max-depth=1 /var/<span class="hljs-built_in">log</span> | sort -hr
</code></pre>
<p>This will show you the size of each immediate subdirectory, sorted from largest to smallest.</p>
<h3 id="heading-3-find-locate-large-files">3. <code>find</code> — Locate Large Files</h3>
<p>Sometimes you just need to hunt down the big files:</p>
<pre><code class="lang-bash">find /var -<span class="hljs-built_in">type</span> f -size +100M -<span class="hljs-built_in">exec</span> ls -lh {} \; | sort -k5 -hr
</code></pre>
<p>This finds files larger than 100MB and lists them, sorted by size.</p>
<h2 id="heading-when-to-use-each-tool">When to Use Each Tool</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Tool</strong></td><td><strong>Best Use Case</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>ncdu</code></td><td>Interactive exploration and cleanup</td></tr>
<tr>
<td><code>df</code></td><td>Quick snapshot of disk space across filesystems</td></tr>
<tr>
<td><code>du</code></td><td>Detailed file/directory size summaries (great for scripting)</td></tr>
<tr>
<td><code>find</code></td><td>Laser-targeted search for huge files</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>Keeping an eye on your disk usage is essential for any serious server management strategy.<br /><code>ncdu</code> is an awesome tool for interactive, detailed analysis — especially when paired with options like <code>-x</code> to stay within boundaries.<br />But don't sleep on the classics either: <code>df</code>, <code>du</code>, and <code>find</code> each bring their own strengths to your sysadmin toolkit.</p>
<p>By combining these tools, you’ll stay ahead of disk space problems before they turn into full-blown outages. 🚀</p>
]]></content:encoded></item><item><title><![CDATA[Infrastructure Avengers: What Role Are You Playing in the Tech Universe?]]></title><description><![CDATA[Sometimes, the only way to survive tech culture is to laugh at it.

Recently, I stumbled upon a meme that hit way too close to home. Like, if you've ever touched a bash script, configured a VPC, or even just nodded in agreement during a “high availab...]]></description><link>https://blog.nh4ttruong.me/infrastructure-avengers-what-role-are-you-playing-in-the-tech-universe</link><guid isPermaLink="true">https://blog.nh4ttruong.me/infrastructure-avengers-what-role-are-you-playing-in-the-tech-universe</guid><category><![CDATA[memes]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Site Reliability Engineering]]></category><category><![CDATA[Cloud Engineering ]]></category><category><![CDATA[System Engineering]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Sun, 27 Apr 2025 18:07:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745777058442/353a343b-7d83-43aa-aa13-5a2fc5cdfb64.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Sometimes, the only way to survive tech culture is to laugh at it.</p>
</blockquote>
<p>Recently, I stumbled upon a meme that hit <em>way</em> too close to home. Like, if you've ever touched a bash script, configured a VPC, or even just nodded in agreement during a “high availability” meeting — you’re gonna feel this deep in your soul.</p>
<p>The meme features four Marvel heroes representing four tech roles, and honestly, it’s <em>too</em> accurate.</p>
<h2 id="heading-devops-engineer">DevOps Engineer</h2>
<p>First up, we got DevOps, repped by none other than Thor himself — big muscles, big hammer, <em>even bigger</em> solo energy.<br />DevOps engineers walk into every project like,</p>
<blockquote>
<p><em>"Oh you need CI/CD? Oh you need k8s clusters? Oh you need monitoring? Bet. I got this. Alone."</em></p>
</blockquote>
<p>They’re building pipelines at 2AM, scripting terraform modules <em>mid-flight</em>, and convincing themselves that "it's just a small hotfix" when they’re really rebuilding half the infra.<br />Thor thinks he doesn’t need anyone else — just like every DevOps bro hammering away at Jenkinsfiles thinking they'll achieve world peace.</p>
<p>💬 Reality check? <em>You can't do it all, King. But we admire the hustle.</em></p>
<hr />
<h2 id="heading-cloud-engineer">Cloud Engineer</h2>
<p>Next is our boy, Ant-Man, playing the Cloud Engineer — which is just <em>chef’s kiss</em> perfect.<br />Cloud Engineers literally live for <em>scaling</em>.<br />Auto-scaling groups? Scaling databases vertically and horizontally? Burstable instances? Spot pricing?<br /><strong>If it doesn’t scale, it’s dead to them.</strong></p>
<p>They treat Terraform outputs like sacred scrolls and think "high availability" is a personality trait.</p>
<blockquote>
<p><em>"Bro why would you need a database if you can just scale your read replicas, duh."</em></p>
</blockquote>
<p>Meanwhile, the app is still crashing because someone hardcoded <a target="_blank" href="http://localhost">localhost</a> in production. 🧍‍♂️</p>
<hr />
<h2 id="heading-systems-engineer">Systems Engineer</h2>
<p>Enter: Systems Engineers — the Tony Starks of the tech world.<br />These guys are out here <em>building insane sh</em>t*, making servers dance, automating OS-level magic, and customizing kernels because “it’s more efficient this way.” (??)</p>
<p>Ask a Systems Engineer what they’re working on and they'll hit you with:</p>
<blockquote>
<p><em>"Oh just a tiny side project — a quantum-optimized scheduler that reduces boot time by 0.000001%."</em></p>
</blockquote>
<p>Meanwhile, no one asked, but we’re still lowkey impressed.</p>
<p>They've got racks in their garage, think "uptime" is sexier than abs, and measure worth in uptime percentages.</p>
<hr />
<h2 id="heading-site-reliability-engineer-sre">Site Reliability Engineer (SRE)</h2>
<p>Finally, the SREs. Same Thor energy... but now battle-worn and with an eye patch — because <em>experience</em>.<br />SREs love to <em>pretend</em> they’re not DevOps.</p>
<blockquote>
<p><em>"No no no, I'm an SRE. It's totally different. We use SLIs, SLOs, error budgets... we have standards, man."</em></p>
</blockquote>
<p>Bro, you’re still writing the same bash scripts and fixing the same janky Kubernetes clusters.<br />Just now you have fancier words to justify why the site went down at 3AM.</p>
<p>SREs are like that friend who <em>definitely</em> was a hipster before it was cool, but now insists he’s just “alternative.”<br /><em>You’re still in the same boat as the rest of us, mate.</em></p>
<hr />
<h1 id="heading-conclusion">Conclusion</h1>
<p>At the end of the day, whether you're carrying a hammer, a shrinking suit, a powered exoskeleton, or just a pager... we're all in the same chaotic, beautiful mess called tech.</p>
<p>You’re gonna cry, you’re gonna laugh, you’re gonna scream internally when the CI pipeline fails <em>again</em> for reasons that make no sense.</p>
<p>But hey — at least memes like this one remind us we’re not alone. 🫶</p>
]]></content:encoded></item><item><title><![CDATA[Learn Web Security With PortSwigger]]></title><description><![CDATA[Hey guys ✌🏻, I share my self-study journey in Web Security here, hoping these notes provide something useful for both newcomers and experienced folks.
Summary
In my third year studying Information Security, I realized that what we learn in class is ...]]></description><link>https://blog.nh4ttruong.me/learn-web-security-with-portswigger</link><guid isPermaLink="true">https://blog.nh4ttruong.me/learn-web-security-with-portswigger</guid><category><![CDATA[portswigger]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Sun, 27 Apr 2025 17:46:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745775987620/76efbbc1-d379-4d72-819c-07d50204d928.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey guys ✌🏻, I share my self-study journey in Web Security here, hoping these notes provide something useful for both newcomers and experienced folks.</p>
<h2 id="heading-summary">Summary</h2>
<p>In my third year studying Information Security, I realized that what we learn in class is just the basics. To become a real <code>hacker</code> 👌🏻, self-study and practice are essential.</p>
<p>Feeling unsure and confused, I decided to dive into PortSwigger Labs—the only platform I knew at the time for learning Web Security properly.</p>
<blockquote>
<p><strong>PortSwigger</strong> is a very comprehensive platform for Web Security, with the highlight being <em>PortSwigger Academy</em>—a huge collection of labs where you can practice from basic to advanced levels. I spent over a month working through these labs, which was tough but very rewarding.</p>
</blockquote>
<h2 id="heading-labs-i-completed">Labs I Completed</h2>
<ul>
<li><p><a target="_blank" href="https://www.nh4ttruong.me/post/portswigger-SQL-Injection">SQL Injection Attack</a></p>
</li>
<li><p><a target="_blank" href="https://www.nh4ttruong.me/post/portswigger-file-upload">File Upload Vulnerability Exploit</a></p>
</li>
<li><p><a target="_blank" href="https://www.nh4ttruong.me/post/portswigger-oauth2-vuln">OAuth 2.0 Authentication Attack</a></p>
</li>
<li><p><a target="_blank" href="https://www.nh4ttruong.me/post/portswigger-XSS">XSS Attack</a></p>
</li>
</ul>
<h2 id="heading-hmmm">Hmmm...</h2>
<p>Honestly, I'm <em>pretty lazy</em>. Writing these notes helps me remember and serves as a little diary of my self-study journey in Web Security. If you're also searching for direction, don't hesitate to start with the small steps.l things. Let’s Go!</p>
]]></content:encoded></item><item><title><![CDATA[Struggling with deprecated HorizontalPodAutoscaler API version in Kubernetes +1.26? How to Fix It]]></title><description><![CDATA[In Kubernetes version 1.26 or later, you might have encountered this frustrating error no matches for kind 'HorizontalPodAutoscaler' in version 'autoscaling/v2beta2'. This article will help you automatically fix the API version in your Helm release s...]]></description><link>https://blog.nh4ttruong.me/struggling-with-deprecated-horizontalpodautoscaler-api-version-in-kubernetes-126-how-to-fix-it</link><guid isPermaLink="true">https://blog.nh4ttruong.me/struggling-with-deprecated-horizontalpodautoscaler-api-version-in-kubernetes-126-how-to-fix-it</guid><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Helm]]></category><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Sun, 27 Apr 2025 17:14:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745774996693/40b92110-8724-49a7-938f-9bcba6483a53.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Kubernetes version 1.26 or later, you might have encountered this frustrating error <code>no matches for kind 'HorizontalPodAutoscaler' in version 'autoscaling/v2beta2'</code>. This article will help you automatically fix the API version in your Helm release secrets.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745773903796/f1010521-449d-4314-8937-1c0b73a1ade8.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-tldr">TL;DR</h1>
<p>Suddenly seeing <code>"no matches for kind 'HorizontalPodAutoscaler' in version 'autoscaling/v2beta2'"</code> or <code>"no matches for kind 'HorizontalPodAutoscaler' in version 'autoscaling/v2beta1'"</code> when running Helm upgrades? This happens because these API versions for HPA are deprecated in Kubernetes 1.26+. Use the command below to automatically update the API version in your Helm release secrets:</p>
<pre><code class="lang-bash">curl -sL https://gist.githubusercontent.com/nh4ttruong/172936eed756ca11e60efacf42117d2c/raw/31637ec5fed7a41ae202711304671df2de0bc1cd/fix-api-version.sh | bash -s
</code></pre>
<blockquote>
<p>Always review scripts before piping them directly to bash. The script above updates Helm release secrets to use `autoscaling/v2` instead of deprecated API versions.</p>
</blockquote>
<h1 id="heading-problem">Problem</h1>
<p>If you've recently upgraded your Kubernetes cluster to version 1.26 or later, you might have encountered this frustrating error <code>"no matches for kind 'HorizontalPodAutoscaler' in version 'autoscaling/v2beta2'"</code></p>
<p>This error occurs because Kubernetes 1.26 has officially removed support for the <code>autoscaling/v2beta2</code> API version, which was previously marked as deprecated. The current recommended version is <code>`autoscaling/v2` </code></p>
<p>This issue is particularly problematic when dealing with Helm charts for several reasons:</p>
<ul>
<li><p><strong>Embedded API Versions:</strong> Helm charts often have the API version hard-coded in their templates. When these charts were created with older API versions, they won't automatically adapt to newer Kubernetes versions.</p>
</li>
<li><p><strong>Release History Storage:</strong> Helm stores the deployed resources' state (including API versions) in secrets within your Kubernetes cluster. Even if you update your chart's templates, the previously deployed release information still contains references to the deprecated API.</p>
</li>
<li><p><strong>Update Mechanism Issues</strong>: When you attempt to upgrade a release with <code>helm upgrade</code>, Helm compares the new manifest against the stored release data. If it encounters incompatible API versions, the upgrade fails.</p>
</li>
<li><p><strong>No Automatic Migration</strong>: Helm doesn't automatically migrate resources from deprecated APIs to their replacements, requiring manual intervention.</p>
</li>
</ul>
<p>The result is that even after updating your Helm chart's templates to use <code>autoscaling/v2</code>, you might still face errors because the stored release data references the now-removed <code>autoscaling/v2beta2</code> API.</p>
<h1 id="heading-how-to-fix">How to fix?</h1>
<p>Here's a comprehensive guide to fixing this issue within your Helm releases:</p>
<h2 id="heading-1-identify-the-affected-helm-release">1. Identify the Affected Helm Release</h2>
<p>First, locate the secret that stores your Helm release configuration:</p>
<pre><code class="lang-sh">kubectl get secret -l owner=helm,status=deployed --all-namespaces
</code></pre>
<p>From the output, identify the secret related to your specific Helm release. It will typically follow a naming pattern that includes your release name.</p>
<h2 id="heading-2-extract-and-decode-the-release-configuration">2. Extract and Decode the Release Configuration</h2>
<p>Export the secret to a file:</p>
<pre><code class="lang-sh">kubectl get secret &lt;helm_release_secret&gt; -n &lt;namespace&gt; -o yaml &gt; release.yaml
</code></pre>
<p>Then decode the stored Helm release data:</p>
<pre><code class="lang-sh">cat release.yaml | grep -oP <span class="hljs-string">'(?&lt;=release: ).*'</span> | base64 -d | base64 -d | gzip -d &gt; release.data.decoded
</code></pre>
<p>This command extracts the release data, decodes it from base64 (twice, as Helm doubly encodes it), and decompresses it.</p>
<h2 id="heading-3-modify-the-horizontalpodautoscaler-api-version">3. Modify the HorizontalPodAutoscaler API Version</h2>
<p>Open the decoded file in your preferred text editor:</p>
<pre><code class="lang-sh">nano release.data.decoded  <span class="hljs-comment"># or vim, or any other editor</span>
</code></pre>
<p>Search for all occurrences of <code>autoscaling/v2beta2</code> and replace them with <code>autoscaling/v2</code>.</p>
<p>Be sure to verify that the structure of any HorizontalPodAutoscaler resources is compatible with the v2 API. In particular, note that the metrics specification might need adjustments.</p>
<h2 id="heading-4-re-encode-and-update-the-helm-secret">4. Re-encode and Update the Helm Secret</h2>
<p>After making your changes, you need to re-encode the file:</p>
<pre><code class="lang-sh">cat release.data.decoded | gzip | base64 | base64 | tr -d <span class="hljs-string">'\n'</span> &gt; encoded_release.txt
</code></pre>
<p>Create a patch file named <code>patch.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"release"</span>: <span class="hljs-string">"&lt;NEW_ENCODED_RELEASE_DATA&gt;"</span>
  }
}
</code></pre>
<p>Replace <code>&lt;NEW_ENCODED_RELEASE_DATA&gt;</code> with the content of your <code>encoded_release.txt</code> file.</p>
<p>Apply the patch to update the Helm release secret:</p>
<pre><code class="lang-sh">kubectl patch secret &lt;helm_release_secret&gt; -n &lt;namespace&gt; --patch-file=patch.json
</code></pre>
<h2 id="heading-5-verify-and-redeploy">5. Verify and Redeploy</h2>
<p>Finally, run a Helm upgrade to ensure the fix is applied:</p>
<pre><code class="lang-sh">helm upgrade &lt;release_name&gt; &lt;chart_name&gt; -n &lt;namespace&gt;
</code></pre>
<p>To verify that the HorizontalPodAutoscaler is now using the correct API version:</p>
<pre><code class="lang-sh">kubectl get hpa -n &lt;namespace&gt; -o yaml | grep apiVersion
</code></pre>
<p>This should show <code>apiVersion: autoscaling/v2</code> for your HorizontalPodAutoscaler resources.</p>
<h1 id="heading-prevention-measures">Prevention Measures</h1>
<p>To avoid similar issues in the future:</p>
<ol>
<li><p><strong>Stay Informed About API Deprecations</strong>: Before upgrading Kubernetes, always check the release notes for any APIs that are deprecated and will be removed.</p>
</li>
<li><p><strong>Update Your Helm Charts</strong>: Make sure your charts are regularly updated to support the latest Kubernetes API versions.</p>
</li>
<li><p><strong>Use Automated Tools</strong>: Consider using tools like <a target="_blank" href="https://github.com/FairwindsOps/pluto">pluto</a> to find deprecated API usage in your cluster before upgrading.</p>
</li>
<li><p><strong>Test in Non-Production</strong>: Always test Kubernetes upgrades in a staging environment first to identify these kinds of issues.</p>
</li>
<li><p><strong>Helm Chart Maintenance</strong>: For teams maintaining their own charts, set up a process to regularly review and update API versions.</p>
</li>
</ol>
<h1 id="heading-conclusion">Conclusion</h1>
<p>API deprecations are a common challenge when upgrading Kubernetes, and Helm's release storage can make these issues especially difficult to solve. By learning how to diagnose and fix these issues within Helm's stored release data, you can ensure smoother upgrades and reduce disruptions to your services.</p>
<p>This specific fix for the HorizontalPodAutoscaler API version issue shows how important it is to stay updated with Kubernetes changes while keeping your deployment tools like Helm compatible.</p>
<p>Have you faced other interesting challenges when upgrading Kubernetes or working with Helm charts? Let me know.</p>
]]></content:encoded></item><item><title><![CDATA[S-SDLC -  A Part Of DevSecOps Journey]]></title><description><![CDATA["Security is not a checkbox. It's a mindset."

In today’s world of high-speed software development, traditional security models just don’t cut it anymore.That’s where S-SDLC (Secure Software Development Life Cycle) steps in — it's not just a buzzword...]]></description><link>https://blog.nh4ttruong.me/s-sdlc-a-part-of-devsecops-journey</link><guid isPermaLink="true">https://blog.nh4ttruong.me/s-sdlc-a-part-of-devsecops-journey</guid><dc:creator><![CDATA[Nhật Trường]]></dc:creator><pubDate>Sun, 27 Apr 2025 16:05:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745769905445/f787435f-54b8-4d9e-a274-ef3174592a4f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>"Security is not a checkbox. It's a mindset."</p>
</blockquote>
<p>In today’s world of <strong>high-speed software development</strong>, traditional security models just don’t cut it anymore.<br />That’s where <strong>S-SDLC</strong> (Secure Software Development Life Cycle) steps in — it's not just a buzzword, it’s an absolute <strong>must-have</strong> if you're serious about DevSecOps.</p>
<h2 id="heading-what-the-heck-is-s-sdlc-anyway">What the Heck is S-SDLC Anyway?</h2>
<p>At its core, <strong>S-SDLC</strong> is about <strong>integrating security at every single stage</strong> of your software development process — from requirements gathering all the way to production monitoring.</p>
<p>It flips the script from <strong>"secure it later"</strong> to <strong>"build it secure from the start"</strong>. No more slapping on firewalls at the end and praying hackers don't show up.</p>
<p>Here’s the typical stages:</p>
<ol>
<li><p><strong>Requirements Gathering</strong> (define security needs)</p>
</li>
<li><p><strong>Design</strong> (identify threats, create secure architectures)</p>
</li>
<li><p><strong>Implementation</strong> (secure coding practices)</p>
</li>
<li><p><strong>Testing</strong> (automated security testing, SAST/DAST tools)</p>
</li>
<li><p><strong>Deployment</strong> (secure configs, container hardening)</p>
</li>
<li><p><strong>Maintenance</strong> (continuous monitoring, patching)</p>
</li>
</ol>
<p>S-SDLC isn’t a replacement for DevOps or DevSecOps — it’s a <strong>core part</strong> of how they evolve.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745769349657/0058523c-833f-4085-b4b7-9c90fda11a71.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-should-you-even-care">Why Should You Even Care?</h2>
<p>Because the old way was broken. Waiting until production to think about security is like building a car and checking if it has brakes <em>after</em> you hit the highway. 🚗💨</p>
<p>S-SDLC helps you:</p>
<ul>
<li><p>Catch security flaws early (when they're cheap to fix)</p>
</li>
<li><p>Build trust with customers and stakeholders</p>
</li>
<li><p>Comply with standards (ISO, GDPR, HIPAA, you name it)</p>
</li>
<li><p>Sleep better at night knowing your apps aren't a hacker’s playground</p>
</li>
</ul>
<hr />
<h2 id="heading-real-life-examples-of-s-sdlc-in-action">Real-Life Examples of S-SDLC in Action</h2>
<p>Here’s where the rubber meets the road. Let’s break down some <strong>real-world S-SDLC practices</strong>:</p>
<h3 id="heading-shifting-security-left">🛡️ Shifting Security Left</h3>
<p>During the <strong>Requirements Phase</strong>, teams set clear security goals like:</p>
<blockquote>
<p>"All customer data must be encrypted at rest using AES-256."</p>
</blockquote>
<p>No more vague "we’ll think about security later" BS. Specific, measurable, enforceable requirements, from Day 1.</p>
<h3 id="heading-threat-modeling-before-coding">🧠 Threat Modeling Before Coding</h3>
<p>Before a single line of code drops, teams run <strong>Threat Modeling</strong> sessions.</p>
<p>Example:</p>
<ul>
<li><p>Identify spoofing risks in login flows.</p>
</li>
<li><p>Spot data tampering possibilities in APIs.</p>
</li>
</ul>
<p>Use tools like <strong>OWASP Threat Dragon</strong> or just good ol' whiteboard sessions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745769397844/dc4e607f-bbb8-4e5f-b410-ed0b002e5652.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-secure-coding-standards">✍️ Secure Coding Standards</h3>
<p>During development, engineers follow secure coding guidelines, such as:</p>
<ul>
<li><p>Parameterizing SQL queries (to avoid injections)</p>
</li>
<li><p>Validating all user inputs</p>
</li>
<li><p>Escaping outputs properly in web apps</p>
</li>
<li><p>…</p>
</li>
</ul>
<p>Think <a target="_blank" href="https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/">OWASP Secure Coding Practices</a> — not "cowboy coding."</p>
<hr />
<h3 id="heading-security-testing-in-cicd">🚀 Security Testing in CI/CD</h3>
<p>Security isn't some final boss fight at the end — it’s baked right into your pipelines:</p>
<ul>
<li><p>Static Application Security Testing (<strong>SAST</strong>) tools like SonarQube</p>
</li>
<li><p>Dynamic Application Security Testing (<strong>DAST</strong>) like OWASP ZAP</p>
</li>
<li><p>Dependency vulnerability scans with Snyk or Dependabot</p>
</li>
</ul>
<p>If your pipeline ain't yelling about vulnerabilities, you’re doing it wrong.</p>
<hr />
<h3 id="heading-managing-third-party-dependencies">🛠️ Managing Third-Party Dependencies</h3>
<p>Third-party libraries can be sneaky — one bad package update, and boom 💥. S-SDLC enforces:</p>
<ul>
<li><p>Continuous monitoring of dependencies</p>
</li>
<li><p>Blocking known vulnerable libraries from builds</p>
</li>
<li><p>Automated patching where possible</p>
</li>
</ul>
<hr />
<h3 id="heading-continuous-monitoring-after-deployment">👀 Continuous Monitoring After Deployment</h3>
<p>Even after it's live, security doesn't stop. Use runtime security tools to collect and analyze logs from your hosts and your deployment... (Ex: Elasticsearch with Auditbeat, Winlogbeat; Tragon)</p>
<p>It’s not just "deploy and hope" anymore — it’s "deploy and watch like a hawk."</p>
<hr />
<h2 id="heading-s-sdlc-and-devsecops-the-power-couple">S-SDLC and DevSecOps — The Power Couple 💍</h2>
<p>S-SDLC is a huge chunk of the <strong>DevSecOps mindset</strong>. You can’t automate what you don’t plan for. And you can’t "shift left" without a secure foundation.</p>
<p><strong>DevSecOps</strong> = DevOps + Security Everywhere.<br /><strong>S-SDLC</strong> = The game plan to actually make that happen.</p>
<p>One’s the vision, the other’s the execution.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>If you want to truly live that <strong>DevSecOps life</strong>, you can’t treat security like a side quest. You have to <strong>build it in</strong> — everywhere, always.</p>
<p><strong>S-SDLC</strong> makes that happen, making sure security is not just a "thing" you tack on, but the way you build, test, and run your software.</p>
<p>No excuses. No shortcuts. Just solid, secure apps from start to finish.</p>
]]></content:encoded></item></channel></rss>