<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Ef&#39;s log</title>
    <link>https://fahmifj.github.io/</link>
    <description>Recent content on Ef&#39;s log</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Wed, 10 Jun 2026 13:50:21 +0700</lastBuildDate><atom:link href="https://fahmifj.github.io/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PNETLab on Google Cloud – Part 1: Server Deployment</title>
      <link>https://fahmifj.github.io/blog/deploy-pnetlab-on-gcp-part-1/</link>
      <pubDate>Wed, 26 Mar 2025 13:27:29 +0700</pubDate>
      
      <guid>https://fahmifj.github.io/blog/deploy-pnetlab-on-gcp-part-1/</guid>
      <description>Introduction While running GNS3 inside Orbstack, I encountered an issue with Orbstack&amp;rsquo;s networking system that prevented my localhost from communicating with GNS3 appliances. Instead of spending more time troubleshooting something that is limited by its design, I decided to deploy a cloud-hosted network lab using PNETLab.
The lab infrastructure is provisioned using Terraform and remote access to the lab is secured through Tailscale instead of exposing it directly to the Internet.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>While running <a href="/articles/running-gns3-on-apple-m2-orbstack/"target="_blank" rel="noopener noreferrer"
>GNS3 inside Orbstack</a>, I encountered an issue with Orbstack&rsquo;s networking system that prevented my localhost from communicating with GNS3 appliances. Instead of spending more time troubleshooting something that is limited by its design, I decided to deploy a cloud-hosted network lab using PNETLab.</p>
<p>The lab infrastructure is provisioned using Terraform and remote access to the lab is secured through Tailscale instead of exposing it directly to the Internet.</p>
<p>This article covers the deployment process. The remote access setup will be covered in a separate article.</p>
<h3 id="goals">Goals</h3>
<ul>
<li>Build reusable PNETLab server with Terraform.</li>
</ul>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>GCP &amp; Tailscale accounts.</li>
<li>GCP CLI installed.</li>
<li>Terraform installed.</li>
</ul>
<h2 id="architecture-overview"><strong>Architecture Overview</strong></h2>
<p>Here’s an overview of what we’re going to build:</p>
<img src="./imgs/image-20251028001404879.png" alt="image-20251028001404879" style="zoom:50%;" />
<p>The setup consists of:</p>
<ul>
<li><strong>Terraform for infrastructure automation</strong>.</li>
<li><strong>A GCP compute instance running PNETLab</strong>.</li>
<li><strong>A private VPC network</strong>.</li>
<li><strong>Cloud Router for outbound internet access (dynamic IP)</strong>.</li>
<li>Tailscale for secure remote access to PNETLab instance</li>
</ul>
<h2 id="prepare-the-gcp-environment">Prepare the GCP Environment</h2>
<p>Before we begin, several prerequisites need to be prepared within Google Cloud Platform.</p>
<blockquote>
<p>Note: I&rsquo;m using GCP&rsquo;s free trial credits for this setup, if you&rsquo;re on a paid account, the steps remain the same.</p>
</blockquote>
<h3 id="gcp-authentication">GCP Authentication</h3>
<p>Terraforms requires credentials to access your Google Cloud resources. There are several methods for this but I will be using Application Default Credentials (ADC) method for this personal project.</p>
<p>From CLI, run the following:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud auth application-default login</span></span></code></pre>
</figure><p>A browser window will pop ups, requesting for Google account authentication. After a successful authtentication, the credentials will be stored in the default location and Terraform will look into it automatically.</p>
<h3 id="creating-a-gcp-project">Creating a GCP Project</h3>
<p>First, create a new project for our PNETLab deployment using GCP CLI.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nv">PROJECT_ID</span><span class="o">=</span>pnetlab-<span class="nv">$RANDOM</span>
</span></span><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">pnetlab-xxx
</span></span><span class="line"><span class="cl">$ mkdir <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ <span class="nb">cd</span> <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ gcloud projects create <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ gcloud config <span class="nb">set</span> project <span class="nv">$PROJECT_ID</span></span></span></code></pre>
</figure><p>Then, link the project to your <a href="https://cloud.google.com/billing/docs/how-to/create-billing-account"target="_blank" rel="noopener noreferrer"
>billing account</a>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud billing accounts list              
</span></span><span class="line"><span class="cl">ACCOUNT_ID            NAME                OPEN  MASTER_ACCOUNT_ID
</span></span><span class="line"><span class="cl">AAAAAA-BBBBBB-CCCCCC  My Billing Account  True
</span></span><span class="line"><span class="cl">$ gcloud billing projects link <span class="nv">$PROJECT_ID</span> --billing-account<span class="o">=</span>AAAAAA-BBBBBB-CCCCCC</span></span></code></pre>
</figure><h3 id="enabling-compute-engine-api">Enabling Compute Engine API</h3>
<p>To allow Terraform to manage VM resources, we must enable the Compute Engine API.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud services <span class="nb">enable</span> compute.googleapis.com</span></span></code></pre>
</figure><h2 id="server-deployment-with-terraform">Server Deployment with Terraform</h2>
<p>We will use Terraform to provision our server that will host PNETLab.</p>
<h3 id="preparing-project-structure">Preparing Project Structure</h3>
<p>Create the following project structure.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ tree
</span></span><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── README.MD
</span></span><span class="line"><span class="cl">└── terraform/
</span></span><span class="line"><span class="cl">    ├── terraforms.tfvars <span class="c1"># sensitive informations goes here (project_id, region, user/keys)</span>
</span></span><span class="line"><span class="cl">    ├── main.tf <span class="c1"># defined gcp resources</span>
</span></span><span class="line"><span class="cl">    └── variables.tf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="m">1</span> directories, <span class="m">4</span> files</span></span></code></pre>
</figure><h4 id="terraformtfvars">terraform.tfvars</h4>
<p><code>terraforms.tfvars</code> contains the following:</p>
<figure class="highlight">
    <pre tabindex="0"><code class="language-" data-lang="">project_id      = &#34;&#34;
region          = &#34;&#34;
zone            = &#34;&#34;
os_image        = &#34;projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20240116a&#34;</code></pre>
</figure><p>Other than <code>os_image</code>, make your adjustment. We use older Ubuntu because the PNETLab core service</p>
<h4 id="maintf">main.tf</h4>
<p><code>main.tf</code> file contains the following:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">terraform</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">required_providers</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">google</span> <span class="err">=</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">      <span class="err">source</span> <span class="err">=</span> <span class="nt">&#34;hashicorp/google&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">version</span> <span class="err">=</span> <span class="s2">&#34;~&gt; 4.5&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="err">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">provider</span> <span class="s2">&#34;google&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">project</span> <span class="err">=</span> <span class="err">var.project_id</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span>  <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">zone</span>    <span class="err">=</span> <span class="err">var.zone</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">NETWORK</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_network&#34;</span> <span class="s2">&#34;pnetlab_vpc&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>                    <span class="err">=</span> <span class="nt">&#34;pnetlab-vpc&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">auto_create_subnetworks</span> <span class="err">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_subnetwork&#34;</span> <span class="s2">&#34;pnetlab_subnet&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>          <span class="err">=</span> <span class="nt">&#34;pnetlab-subnet&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span>       <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">  <span class="err">ip_cidr_range</span> <span class="err">=</span> <span class="s2">&#34;10.10.1.0/24&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_firewall&#34;</span> <span class="s2">&#34;allow_ssh_access&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>    <span class="err">=</span> <span class="nt">&#34;allow-ssh&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span> <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">allow</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">protocol</span> <span class="err">=</span> <span class="nt">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">ports</span>    <span class="err">=</span> <span class="p">[</span><span class="s2">&#34;22&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">source_ranges</span> <span class="err">=</span> <span class="p">[</span> <span class="s2">&#34;0.0.0.0/0&#34;</span> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">NAT</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_router&#34;</span> <span class="s2">&#34;pnetlab_router&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span> <span class="err">=</span> <span class="nt">&#34;pnetlab-router&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span> <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span> <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_router_nat&#34;</span> <span class="s2">&#34;pnetlab_router_nat&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span> <span class="err">=</span> <span class="nt">&#34;pnetlab-router-nat&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span> <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">router</span> <span class="err">=</span> <span class="err">google_compute_router.pnetlab_router.name</span>
</span></span><span class="line"><span class="cl">  <span class="err">nat_ip_allocate_option</span> <span class="err">=</span> <span class="s2">&#34;AUTO_ONLY&#34;</span> <span class="err">#</span> <span class="err">use</span> <span class="err">dynamic</span> <span class="err">public</span> <span class="err">ip</span> <span class="err">for</span> <span class="err">nat</span>
</span></span><span class="line"><span class="cl">  <span class="err">source_subnetwork_ip_ranges_to_nat</span> <span class="err">=</span> <span class="s2">&#34;ALL_SUBNETWORKS_ALL_IP_RANGES&#34;</span> <span class="err">#</span> <span class="err">nat</span> <span class="err">any</span> <span class="err">sources</span> <span class="err">ip</span> <span class="err">range</span> <span class="err">available</span> <span class="err">within</span> <span class="err">the</span> <span class="err">vpc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">EXT</span> <span class="err">DISK</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_disk&#34;</span> <span class="s2">&#34;additional_disk&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>   <span class="err">=</span> <span class="nt">&#34;pnetlab-additional-disk&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>   <span class="err">=</span> <span class="s2">&#34;pd-standard&#34;</span>  <span class="err">#</span> <span class="err">Choose</span> <span class="err">disk</span> <span class="err">type</span> <span class="err">(pd-standard</span><span class="p">,</span> <span class="err">pd-ssd,</span> <span class="err">etc.)</span>
</span></span><span class="line"><span class="cl">  <span class="err">size</span> <span class="err">=</span> <span class="err">50</span> 
</span></span><span class="line"><span class="cl">  <span class="err">lifecycle</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">prevent_destroy</span> <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">INSTANCES</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">ubuntu</span><span class="mi">-1804</span><span class="err">-bionic-v</span><span class="mi">20240116</span><span class="err">a</span>                                 <span class="err">ubuntu-os-cloud</span>      <span class="err">ubuntu</span><span class="mi">-1804</span><span class="err">-lts</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_instance&#34;</span> <span class="s2">&#34;pnetlab_server&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>         <span class="err">=</span> <span class="nt">&#34;pnetlab-server&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">machine_type</span> <span class="err">=</span> <span class="s2">&#34;n2-standard-2&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">tags</span>         <span class="err">=</span> <span class="p">[</span><span class="s2">&#34;pnetlab&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">boot_disk</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">initialize_params</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">      <span class="err">image</span> <span class="err">=</span> <span class="err">var.os_image</span> <span class="err">#</span> <span class="err">cloud</span> <span class="err">image</span>
</span></span><span class="line"><span class="cl">      <span class="err">size</span>  <span class="err">=</span> <span class="err">20</span>                 <span class="err">#</span> <span class="err">assign</span> <span class="err">20gb</span>
</span></span><span class="line"><span class="cl">      <span class="err">type</span>  <span class="err">=</span> <span class="nt">&#34;pd-standard&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="err">#</span> <span class="err">external</span> <span class="err">disk</span> <span class="mi">50</span><span class="err">gb</span>
</span></span><span class="line"><span class="cl">  <span class="err">attached_disk</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">source</span> <span class="err">=</span> <span class="err">google_compute_disk.additional_disk.id</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">network_interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">network</span>    <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">    <span class="err">subnetwork</span> <span class="err">=</span> <span class="err">google_compute_subnetwork.pnetlab_subnet.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="err">Disable</span> <span class="err">public</span> <span class="err">IP</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="err">access_config</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span>  <span class="err">network_tier</span> <span class="err">=</span> <span class="nt">&#34;STANDARD&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">scheduling</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">automatic_restart</span>           <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">    <span class="err">on_host_maintenance</span>         <span class="err">=</span> <span class="nt">&#34;MIGRATE&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">preemptible</span>                 <span class="err">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">    <span class="err">provisioning_model</span>          <span class="err">=</span> <span class="s2">&#34;STANDARD&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">advanced_machine_features</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">enable_nested_virtualization</span> <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="err">}</span></span></span></code></pre>
</figure><p>This configuration provisions:</p>
<ul>
<li>Custom VPC network.</li>
<li>Subnet.</li>
<li>Firewall rules.</li>
<li>Cloud NAT router.</li>
<li>Additional persistent disk.</li>
<li>Enabled nested virtualization (need machine type that support nested virtualization).</li>
</ul>
<p>There&rsquo;s also n1 for alternative machine type if you worry about the cost, lets say <code>n1-custom-1-2048</code>, which means machine type is <code>n1</code> with single core and  2 GB of RAM.</p>
<h4 id="variablestf">variables.tf</h4>
<p><code>variables.tf</code> define the variables we used in the <code>tfvars</code> file, it contains:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;project_id&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project ID&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;region&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project region&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">default</span>     <span class="err">=</span> <span class="s2">&#34;us-central&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;zone&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project zone&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">default</span>     <span class="err">=</span> <span class="s2">&#34;us-central1-a&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;os_image&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;OS family&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</figure><h3 id="vm-deployment">VM Deployment</h3>
<p>Within the terraform folder, initialize the terraform working directory.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ terraform init</span></span></code></pre>
</figure><p>The init step only needs to be performed be performed during the first deployment or after provider/module changes.</p>
<p>With all the resource files defined, we can start the deployment.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ terraform fmt         <span class="c1"># Format your Terraform files  </span>
</span></span><span class="line"><span class="cl">$ terraform validate    <span class="c1"># Check for syntax errors  </span>
</span></span><span class="line"><span class="cl">$ terraform plan -out pnetlab-deploy       <span class="c1">#Preview the infrastructure changes  </span>
</span></span><span class="line"><span class="cl">$ terraform apply pnetlab-deploy  <span class="c1"># Deploy the resources  </span></span></span></code></pre>
</figure><h3 id="verify-deployment">Verify Deployment</h3>
<p>After it finishes, check if the instance is running:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud compute instances list --filter<span class="o">=</span><span class="s2">&#34;name=pnetlab-server&#34;</span> --project<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$PROJECT_ID</span><span class="s2">&#34;</span></span></span></code></pre>
</figure><blockquote>
<p>Please mind that the terraform config I provide here is only reusable for personal project, it&rsquo;s not reusable for team scale project.</p>
</blockquote>
<h2 id="pnetlab-installation">PNETLab Installation</h2>
<h3 id="install-pnetlab">Install PNETLab</h3>
<p>SSH into the instance using <code>gcloud</code> cli.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud compute ssh INSTANCE_NAME --zone<span class="o">=</span>YOUR_ZONE</span></span></code></pre>
</figure><p>Add pnetlab repository with the following command.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;deb [trusted=yes] http://repo.pnetlab.com ./&#34;</span> <span class="p">|</span> sudo tee -a /etc/apt/sources.list</span></span></code></pre>
</figure><p>Update the repository and install pnetlab.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo apt-get update
</span></span><span class="line"><span class="cl">$ sudo apt-get install pnetlab</span></span></code></pre>
</figure><p>During installation process, you might be asked to replace kernel configuration like this</p>
<p><div class="img-container"><img src="./imgs/image-20260528204224869.png" alt="image-20260528204224869"  /></div>
</p>
<p>Just go with it and continue the installation.</p>
<p>Once it finished, you will have a name resolution/DNS issue, but that&rsquo;s fine for now.</p>
<h3 id="server-configuration">Server Configuration</h3>
<h4 id="swap">Swap</h4>
<p>Create a swap disk, allocate the size according to your need.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo fallocate -l 1G /swapfile
</span></span><span class="line"><span class="cl">$ sudo chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="cl">$ sudo mkswap /swapfile
</span></span><span class="line"><span class="cl">$ sudo swapon /swapfile
</span></span><span class="line"><span class="cl">$ sudo cp /etc/fstab<span class="o">{</span>,.bak<span class="o">}</span>
</span></span><span class="line"><span class="cl">$ sudo <span class="s1">&#39;/swapfile none swap sw 0 0&#39;</span> <span class="p">|</span> sudo tee -a /etc/fstab</span></span></code></pre>
</figure><h4 id="dns">DNS</h4>
<p>Edit file <code>/etc/network/interfaces</code> add line <code>dns-nameservers 8.8.8.8</code> to the configuration of interface <code>pnet0</code>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="c"># The primary network interface</span>
</span></span><span class="line"><span class="cl"><span class="nx">iface</span> <span class="nx">eth0</span> <span class="nx">inet</span> <span class="nx">manual</span>
</span></span><span class="line"><span class="cl"><span class="nx">auto</span> <span class="nx">pnet0</span>
</span></span><span class="line"><span class="cl"><span class="nx">iface</span> <span class="nx">pnet0</span> <span class="nx">inet</span> <span class="nx">dhcp</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dns-nameservers</span> <span class="mf">8.8</span><span class="p">.</span><span class="mf">8.8</span>	
</span></span><span class="line"><span class="cl">    <span class="nx">bridge_ports</span> <span class="nx">eth0</span>
</span></span><span class="line"><span class="cl">    <span class="nx">bridge_stp</span> <span class="nx">off</span>
</span></span><span class="line"><span class="cl"><span class="p">...</span></span></span></code></pre>
</figure><p>And restart the networking service.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo service networking restart</span></span></code></pre>
</figure><h4 id="disk">Disk</h4>
<p>Time to add the additional disk we defined in the terraform resource. The purpose of this disk is to store all the pnetlab project files as well as the images later. We can preserve the lab data in this disk in case the VM need to be replaced/gone wrong, we will get into the detail later.</p>
<p>In my case, the additional disk is available at <code>/dev/sdb</code>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo lsblk
</span></span><span class="line"><span class="cl">NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
</span></span><span class="line"><span class="cl">loop0     7:0    <span class="m">0</span> 40.4M  <span class="m">1</span> loop /snap/snapd/20671
</span></span><span class="line"><span class="cl">loop1     7:1    <span class="m">0</span>  368M  <span class="m">1</span> loop /snap/google-cloud-cli/203
</span></span><span class="line"><span class="cl">loop2     7:2    <span class="m">0</span> 63.9M  <span class="m">1</span> loop /snap/core20/2105
</span></span><span class="line"><span class="cl">sda       8:0    <span class="m">0</span>   20G  <span class="m">0</span> disk 
</span></span><span class="line"><span class="cl">├─sda1    8:1    <span class="m">0</span> 19.9G  <span class="m">0</span> part /
</span></span><span class="line"><span class="cl">├─sda14   8:14   <span class="m">0</span>    4M  <span class="m">0</span> part 
</span></span><span class="line"><span class="cl">└─sda15   8:15   <span class="m">0</span>  106M  <span class="m">0</span> part /boot/efi
</span></span><span class="line"><span class="cl">sdb       8:16   <span class="m">0</span>  100G  <span class="m">0</span> disk <span class="c1"># External Disk</span></span></span></code></pre>
</figure><p>We&rsquo;ll create an LVM disk out of it.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo vgcreate /dev/vg01 /dev/sdb
</span></span><span class="line"><span class="cl">$ sudo lvcreate -l 100%FREE --name lv01 vg01 
</span></span><span class="line"><span class="cl">$ sudo mkfs.ext4 /dev/vg01/lv01 </span></span></code></pre>
</figure><p>Then we set a mount point for the volume we just created at <code>/mnt/unetlab</code></p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab
</span></span><span class="line"><span class="cl">$ sudo mount /dev/vg01/lv01 /mnt/unetlab</span></span></code></pre>
</figure><p>Edit the <code>/etc/fstab</code> file and add the following line to mount <code>/mnt/unetlab</code> on boot:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">/dev/vg01/lv01  /mnt/unetlab  ext4  defaults  0  0</span></span></span></code></pre>
</figure><p>Now we&rsquo;ll create a new directory for PNETLab project files and image files in our newly mounted disk.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab/addons
</span></span><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab/labs</span></span></code></pre>
</figure><p>Next, we&rsquo;ll remove the old directories for project files and image files, and replace them with symbolic links pointing to the new directories on the mounted disk.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo rm -rf /opt/unetlab/addons/ 
</span></span><span class="line"><span class="cl">$ sudo rm  -rf /opt/unetlab/labs/
</span></span><span class="line"><span class="cl">$ sudo ln -s /mnt/unetlab/addons/ /opt/unetlab/
</span></span><span class="line"><span class="cl">$ sudo ln -s /mnt/unetlab/labs/ /opt/unetlab/</span></span></code></pre>
</figure><p>Change ownership of the new labs directory to ensure PNETLab&rsquo;s services can access it back.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo chown -R www-data:www-data /mnt/unetlab/labs/</span></span></code></pre>
</figure><h4 id="grub">Grub</h4>
<p>Remove the default grub config.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo rm /etc/default/grub.d/50-cloudimg-settings.cfg
</span></span><span class="line"><span class="cl">$ sudo update-grub</span></span></code></pre>
</figure><h4 id="restart-server">Restart Server</h4>
<p>To complete the installation, restart the server.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo reboot</span></span></code></pre>
</figure><p>Once you logged in, there will be a pop up asking for setting root password, hostname, and NTP. Configure these according to your preferences.</p>
<p>We also need to make sure that the internet connectivity is working with simple <code>ping</code> check to <code>1.1.1.1</code>.</p>
<h3 id="verify-installation">Verify Installation</h3>
<p>Since the server configured without a public IP address, the PNETLab web interface cannot be accessed directly from the internet. The installation can be validated locally using <code>curl</code> from the server.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ curl -k https://localhost</span></span></code></pre>
</figure><p>Expected output:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">HTTP/1.1 <span class="m">301</span> Moved Permanently
</span></span><span class="line"><span class="cl">Date: Thu, <span class="m">28</span> May <span class="m">2026</span> 15:02:55 GMT
</span></span><span class="line"><span class="cl">Server: Apache/2.4.29 <span class="o">(</span>Ubuntu<span class="o">)</span>
</span></span><span class="line"><span class="cl">Access-Control-Allow-Origin: *
</span></span><span class="line"><span class="cl">Access-Control-Allow-Headers: *
</span></span><span class="line"><span class="cl">Location: /store/public/admin/main/view
</span></span><span class="line"><span class="cl">...</span></span></code></pre>
</figure><h3 id="update-version-optional">Update Version (Optional)</h3>
<p>After the installation completed, we can directly upgrade PNETLab to the latest version. It&rsquo;s an optional, so I will just put the official guide link on how to upgrade the lab version: <a href="https://pnetlab.com/pages/releases"target="_blank" rel="noopener noreferrer"
>go here</a>.</p>
<h2 id="troubleshoot">Troubleshoot</h2>
<p>I know things that may go wrong before you finish it, so there are a few problems I encountered and the solutions.</p>
<h3 id="cloud-appliance-dhcp-not-working">Cloud Appliance: DHCP not working</h3>
<p>Verify <code>udhcpd</code> service is running and enabled.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ service udhcpd status
</span></span><span class="line"><span class="cl">$ cat /etc/default/udhcpd <span class="p">|</span> grep ENABLED</span></span></code></pre>
</figure><p>Set to enabled and restart to see if it&rsquo;s working.</p>
<h2 id="conclusion">Conclusion</h2>
<p>We have completed the deployment process of PNETLab server on Google Cloud using Terraform. At this stage, the lab is running internally but remains inaccessible from outside the private network.</p>
<p>In the next article, we will configure Tailscale to provide secure remote access to the lab.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Blog - This Website</title>
      <link>https://fahmifj.github.io/projects/development/blog/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://fahmifj.github.io/projects/development/blog/</guid>
      <description>A personal blog built with Hugo and a heavily customized base theme. The blog itself is an ongoing project.</description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>Sample Pentest Report for THM Wreath</title>
      <link>https://fahmifj.github.io/projects/security/pentest-report-wreath/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://fahmifj.github.io/projects/security/pentest-report-wreath/</guid>
      <description>A penetration test report for TryHackMe wreath.</description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>Building Virtual Home Lab for Pentesting</title>
      <link>https://fahmifj.github.io/blog/building-virtual-home-lab-for-pentest/</link>
      <pubDate>Thu, 17 Jun 2021 14:04:24 +0700</pubDate>
      
      <guid>https://fahmifj.github.io/blog/building-virtual-home-lab-for-pentest/</guid>
      <description>Virtual home lab for offensive security practice, with 8 gigs!</description>
      <content:encoded><![CDATA[<p>This article documents a virtual Active Directory lab I built to learn some Active Directory attacks, network pivoting, and basic Command and Control (<code>C2</code>) using Metasploit.</p>
<p>The lab is designed to resemble a small enterprise Windows environment with minimal requirements that enough to run on a mid-range specification.</p>
<h2 id="assumptions--threat-model">Assumptions &amp; Threat Model</h2>
<p>This lab assumes:</p>
<ul>
<li>An internal attacker with network access</li>
<li>Minimal internal monitoring</li>
<li>Default or weak Windows security configurations</li>
<li>Misconfigured behavior</li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<h3 id="knowledge">Knowledge</h3>
<ul>
<li>Virtualization (VirtualBox)</li>
<li>Windows and Windows Server installation</li>
<li>Basic Active Directory concepts (Domain, DNS, SPNs)</li>
<li>Basic networking and routing</li>
</ul>
<h3 id="hardware">Hardware</h3>
<p>Recommended minimum (sorted by priority):</p>
<ul>
<li><strong>Storage</strong>: 256 GB SSD (or high-speed USB 3.x)</li>
<li><strong>RAM</strong>: 8 GB minimum, 16 GB recommended</li>
<li><strong>CPU</strong>:
<ul>
<li>Minimum: Intel i3 6th gen / Ryzen 3</li>
<li>Recommended: i5 / Ryzen 5 (H or K variants)</li>
</ul>
</li>
</ul>
<blockquote>
<p>The lab was built and tested on an 8 GB system by aggressively disabling unused services after installation.</p>
</blockquote>
<h3 id="software">Software</h3>
<ul>
<li>VirtualBox (<a href="https://www.virtualbox.org/wiki/Downloads"target="_blank" rel="noopener noreferrer"
>Download</a>)</li>
<li>Kali Linux (attacker)  (<a href="https://www.offensive-security.com/kali-linux-vm-vmware-virtualbox-image-download/#1572305786534-030ce714-cc3b"target="_blank" rel="noopener noreferrer"
>Download</a>)</li>
<li>Windows 10 evaluation image file (<a href="https://www.microsoft.com/en-us/evalcenter/"target="_blank" rel="noopener noreferrer"
>Download</a>)</li>
<li>Windows Server 2019 evaluation image file (<a href="https://www.microsoft.com/en-us/evalcenter/"target="_blank" rel="noopener noreferrer"
>Download</a>)</li>
</ul>
<h2 id="topology-overview">Topology Overview</h2>
<p>The environment consists of:</p>
<ul>
<li>1 Domain Controller</li>
<li>2 Domain-joined Windows clients</li>
<li>1 Attacker machine (Kali Linux)</li>
</ul>
<p>Two network segments are used:</p>
<ul>
<li><code>192.168.1.0/24</code> – Internal domain network</li>
<li><code>10.10.10.96/28</code> – Segmented network used for pivoting scenarios</li>
</ul>
<p><div class="img-container"><img src="imgs/topology.jpg" alt=""  /></div>
</p>
<blockquote>
<p>Note: It&rsquo;s NIC (Network Interface Card) not NC</p>
</blockquote>
<h2 id="network-segments">Network Segments</h2>
<p>This lab intentionally uses two network segments to simulate an internal enterprise environment and to enable <strong>pivoting scenarios</strong> later on.</p>
<table>
<thead>
<tr>
<th>Segment</th>
<th>CIDR</th>
<th>Name</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>192.168.1.0/24</code></td>
<td>windows_domain</td>
<td>Main internal network (initial setup &amp; attacker access)</td>
</tr>
<tr>
<td>2</td>
<td><code>10.10.10.96/28</code></td>
<td>internal_windows</td>
<td>Restricted internal segment (pivoting target)</td>
</tr>
</tbody>
</table>
<h2 id="virtualbox-setup">VirtualBox Setup</h2>
<h3 id="system-configuration">System Configuration</h3>
<p>Initial installation:</p>
<ul>
<li>Server: <code>2424 MB</code></li>
<li>Clients: <code>1280 MB</code> each</li>
</ul>
<p>Post-installation (after disabling unnecessary services):</p>
<ul>
<li>Server: <code>1280 MB</code></li>
<li>Clients: <code>1024 MB</code></li>
<li>Attacker: <code>1024 MB</code></li>
</ul>
<p>This configuration allows all VMs to run concurrently on an 8 GB host. For initial setup, the two clients can stay inside <code>192.168.1.0/24</code> network.</p>
<h3 id="network-configuration">Network Configuration</h3>
<p>For each VM (initial setup):</p>
<ul>
<li>Adapter 1
<ul>
<li>Type: <code>Internal Network</code></li>
<li>Name: <code>windows_domain</code></li>
<li>IP Range: <code>192.168.1.0/24</code></li>
</ul>
</li>
</ul>
<p>For the Domain Controller only:</p>
<ul>
<li>Adapter 2
<ul>
<li>Name: <code>internal_windows</code></li>
<li>IP Range: <code>10.10.10.96/28</code></li>
</ul>
</li>
</ul>
<p>This makes the Domain Controller act as a bridge between segments.</p>
<p><div class="img-container"><img src="imgs/image-20210617143401181.png" alt="image-20210617143401181"  /></div>
</p>
<h2 id="active-directory-setup">Active Directory Setup</h2>
<h3 id="server">Server</h3>
<h4 id="initial-setup">Initial Setup</h4>
<ul>
<li>Admin credentials: <code>administrator:p@$$w0rd!</code></li>
<li>PC Name: <code>server19-DC</code> (restart after)</li>
<li>Network (Static):
<ul>
<li>Adapter 1: <code>192.168.1.100/24</code></li>
<li>Adapter 2: <code>10.10.10.100/28</code></li>
</ul>
</li>
</ul>
<h4 id="promote-server-to-domain-controller">Promote Server to Domain Controller</h4>
<ul>
<li>Server Manager &gt; Manage &gt; Add Roles and Features.</li>
<li>Add Roles and Features Wizard:
<ul>
<li>Installation type: &ldquo;<strong>Role-based or feature-based installation</strong>&rdquo;</li>
<li>Server selection: <code>server19-DC</code></li>
<li>Server roles: <strong>&ldquo;Active Directory Domain Services&rdquo;</strong> and check the <strong>&ldquo;Include management tools&rdquo;</strong>.</li>
<li>Features: Check the <strong>&ldquo;Group Policy Management&rdquo;</strong></li>
<li>Confirmation:  Check on <strong>&ldquo;Restart destination server automatically if required&rdquo;</strong></li>
<li>Close after it&rsquo;s done.</li>
</ul>
</li>
<li>Server Manager &gt; Notification flag &gt; Click on <strong>&ldquo;Promote this server to a domain controller&rdquo;</strong></li>
<li>Active Directory Domain Services Configuration Wizard:
<ul>
<li>Deployment configuration: <strong>&ldquo;Add a new forest&rdquo;</strong> and set <strong>&ldquo;server19.local&rdquo;</strong> as root domain name</li>
<li>Domain controller options: set <strong>&ldquo;Windows Server 2016&rdquo;</strong> as FFL (Forest Functional Level) and DFL (Domain Functional Level). Checklist DNS server and set the same admin password for DSRM password.</li>
<li>Additional options: set NetBIOS domain name to <code>SERVER19</code></li>
<li>Let the rest options in default state until installation section.</li>
<li>Restart after installation complete.</li>
</ul>
</li>
</ul>
<p><div class="img-container"><img src="imgs/server19logon.jpg" alt=""  /></div>
</p>
<h4 id="create-domain-accounts">Create Domain Accounts</h4>
<ul>
<li>John Smith
<ul>
<li>User logon name: <code>jsmith@server19.local</code></li>
<li>Password: <code>jsmith@123</code></li>
</ul>
</li>
<li>Carl Smith
<ul>
<li>User logon name: <code>cmisth@server19.local</code></li>
<li>Password: <code>@csmith@</code></li>
</ul>
</li>
</ul>
<p>All password is set to never expires.</p>
<h4 id="create-fake-service-account">Create Fake Service Account</h4>
<p>Fake SQL Service</p>
<ul>
<li>User logon name: <code>SQLService@server19.local</code></li>
<li>Password: <code>Mysql@Password123</code></li>
</ul>
<p>Set service principle name:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">setspn -a SERVER19-DC/SQLService.SERVER19.local:60111 SERVER19\SQLService
</span></span><span class="line"><span class="cl">setspn -T SERVER19.local -Q */*</span></span></code></pre>
</figure><h4 id="configure-file-sharing-smb">Configure File Sharing (SMB):</h4>
<ul>
<li>Server manager &gt; File and Storage Services &gt; Shares &gt; Task &gt; New Share.</li>
<li>New Share Wizard:
<ul>
<li>Profile: SMB Share Quick</li>
<li>Share Location: <code>C:\Shares\DATA</code> (Create the shares folder in C:)</li>
<li>Other Settings: Allow caching of share</li>
<li>Permission: Leave it default</li>
<li>Confirmation and create.</li>
</ul>
</li>
</ul>
<h3 id="clients">Clients</h3>
<h4 id="initial-setup-1">Initial Setup</h4>
<ul>
<li>Client 1:
<ul>
<li>IP: <code>192.168.1.101/24</code> (static)</li>
<li>PC name: NESCOFFEE</li>
</ul>
</li>
<li>Client 2:
<ul>
<li>IP: <code>192.168.1.102/24</code> (static)</li>
<li>PC name: MILO</li>
</ul>
</li>
</ul>
<p>For pivoting</p>
<ul>
<li>Client 2:
<ul>
<li>IP: <code>10.10.10.101/28</code>(static)</li>
</ul>
</li>
</ul>
<h4 id="create-local-accounts">Create Local Accounts</h4>
<p>Same with domain accounts, but add an <code>L</code> at the end of username/password.</p>
<ul>
<li>Username: <code>cmisthL</code>, password: <code>jsmithL@123</code></li>
<li>Username: <code>jsmithL</code>, password: <code>@csmith@</code></li>
</ul>
<h4 id="joining-domain">Joining Domain</h4>
<p>Client 1:</p>
<ul>
<li>Use Server&rsquo;s IP as DNS server: <code>192.168.1.100</code></li>
<li>Hit <code>Win+I</code>, type &ldquo;access&rdquo;, click on <strong>Connect</strong>.</li>
<li>Microsoft account window:
<ul>
<li>Click on <strong>&ldquo;Join this device to a local Active Directory domain&rdquo;</strong> under the alternate actions.</li>
<li>Use the server administrator password to join.</li>
<li>Skip the <strong>Add an account</strong> section</li>
<li>Restart</li>
</ul>
</li>
</ul>
<p>Client 2 has the same steps</p>
<h4 id="create-local-admin">Create Local Admin</h4>
<ul>
<li>Set John Smith (<code>jsmith@server19.local</code>) as local administrator for NESCOFFEE.</li>
<li>Set Carl Smith (<code>cmisth@server19.local</code>) as local administrator for MILO.</li>
</ul>
<h3 id="attacker">Attacker</h3>
<h4 id="initial-setup-2">Initial Setup</h4>
<ul>
<li>Put it on the same network</li>
<li>Set static IP: <code>192.168.1.10/24</code></li>
<li>Perform ping test</li>
</ul>
<h2 id="attack-scenarios">Attack Scenarios</h2>
<p>Here are some attack scenarios that can be reproduced using this lab:</p>
<ul>
<li>
<p>LLMNR Poisoning</p>
<ul>
<li>Example: <a href="https://www.aptive.co.uk/blog/llmnr-nbt-ns-spoofing/"target="_blank" rel="noopener noreferrer"
>https://www.aptive.co.uk/blog/llmnr-nbt-ns-spoofing/</a></li>
</ul>
</li>
<li>
<p>AS-REP Roasting</p>
<ul>
<li>Example: <a href="/tags/asrep-roasting"target="_blank" rel="noopener noreferrer"
>ASREP-Roasting tags</a></li>
</ul>
</li>
<li>
<p>Kerberoasting</p>
<ul>
<li>Example: <a href="https://pentestlab.blog/2018/06/12/kerberoast/"target="_blank" rel="noopener noreferrer"
>https://pentestlab.blog/2018/06/12/kerberoast/</a></li>
</ul>
</li>
<li>
<p>Take Over IPv6 DNS</p>
<ul>
<li>Example:  <a href="https://blog.fox-it.com/2018/01/11/mitm6-compromising-ipv4-networks-via-ipv6/"target="_blank" rel="noopener noreferrer"
>https://blog.fox-it.com/2018/01/11/mitm6-compromising-ipv4-networks-via-ipv6/</a></li>
</ul>
</li>
<li>
<p>DCSync</p>
<ul>
<li>Example: <a href="/tags/dcsync/"target="_blank" rel="noopener noreferrer"
>DCSync tags</a></li>
</ul>
</li>
</ul>
<p>Attack scenario(s) that requires two clients online + server:</p>
<ul>
<li>SMB Relay
<ul>
<li>Examples: <a href="/writeups/hackthebox/htb-apt/"target="_blank" rel="noopener noreferrer"
>HackTheBox - APT</a>, <a href="https://akimboviper.gitbook.io/pentest-everything/everything/everything-windows/attacking-windows/relay-attacks/smb-relay"target="_blank" rel="noopener noreferrer"
>SMB Relay Attack</a></li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Using Cloudflare Workers to Mirror My Blog</title>
      <link>https://fahmifj.github.io/blog/mirror-website-using-cloudflare-workers/</link>
      <pubDate>Sat, 06 Dec 2025 15:33:48 +0700</pubDate>
      
      <guid>https://fahmifj.github.io/blog/mirror-website-using-cloudflare-workers/</guid>
      <description>I want to host my blog on a custom domain that I own (fahmifj.space) instead of using the GitHub Pages domain (fahmifj.github.io).
However, migrating a site to another domain could negatively impact SEO. Although I&amp;rsquo;m not really concerned about it, I think it&amp;rsquo;s a great opportunity to learn something from this case.
So, instead of moving everything, I keep GH Pages as the primary site and let the custom domain act as a mirror.</description>
      <content:encoded><![CDATA[<p>I want to host my blog on a custom domain that I own (<code>fahmifj.space</code>) instead of using the GitHub Pages domain (<code>fahmifj.github.io</code>).</p>
<p>However, migrating a site to another domain could negatively impact SEO. Although I&rsquo;m not really concerned about it, I think it&rsquo;s a great opportunity to learn something from this case.</p>
<p>So, instead of moving everything, I keep GH Pages as the primary site and let the custom domain act as a mirror. Both domains serve the same content without redirecting one to the other.</p>
<p>This post documents the setup and explains why keeping GitHub Pages as the primary works better for me.</p>
<h2 id="cloudflare-workers-as-reverse-proxy">Cloudflare Workers as Reverse Proxy</h2>
<p>Cloudflare provides a feature called Workers. It allows you to write and run serverless code, similar in concept to Google Cloud Functions.</p>
<p>Using this feature, I write a Worker that acts as a reverse proxy. Its job is to fetch my GitHub Pages content and serve it back on the custom domain.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">                       ┌──────────────────────────┐
</span></span><span class="line"><span class="cl">                       │   fahmifj.github.io      │
</span></span><span class="line"><span class="cl">                       │    (upstream hosting)    │
</span></span><span class="line"><span class="cl">                       └──────────┬───────────────┘
</span></span><span class="line"><span class="cl">                                  │ GitHub Pages origin
</span></span><span class="line"><span class="cl">                                  ▼
</span></span><span class="line"><span class="cl">                          Cloudflare Worker
</span></span><span class="line"><span class="cl">                          (proxy + rewrite)
</span></span><span class="line"><span class="cl">                                  │
</span></span><span class="line"><span class="cl">                                  ▼
</span></span><span class="line"><span class="cl">                            fahmifj.space
</span></span><span class="line"><span class="cl">                           (custom domain)</span></span></code></pre>
</figure><h3 id="create-worker-v1">Create Worker (v1)</h3>
<p>This first version of the Worker functions purely as a content fetcher.</p>
<figure class="highlight">
    <figcaption>
        <span class="hl-filename">worker.js</span>
    </figcaption>
    <pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">async</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Force everything to fetch from GitHub Pages 
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">url</span><span class="p">.</span><span class="nx">hostname</span> <span class="o">=</span> <span class="s2">&#34;fahmifj.github.io&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">headers</span><span class="o">:</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">redirect</span><span class="o">:</span> <span class="s2">&#34;follow&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Copy GitHub response and serve it
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="nx">response</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span></span></span></code></pre>
</figure><p>The result is straightforward, it serves the same content as GitHub Pages.</p>
<p><div class="img-container"><img src="./imgs/image-20251208204713163.png" alt="image-20251208204713163"  /></div>
</p>
<h3 id="attach-custom-domain-to-worker">Attach Custom Domain to Worker</h3>
<p>Next, I attach my custom domain to the Worker (<code>Workers &amp; Pages</code> &gt; <code>My Worker</code> &gt; <code>Settings</code> &gt; <code>Domain &amp; Routers</code>).</p>
<img src="./imgs/image-20251209003601514.png" alt="image-20251209003601514" style="zoom: 50%;" />
<p>At this point, <code>fahmifj.space</code> is already serving the same content as <code>fahmifj.github.io</code>.</p>
<h3 id="worker-with-url-rewriter-v2">Worker with URL Rewriter (v2)</h3>
<p>Hugo builds my site with absolute URLs based on my GH Pages domain, which is <code>fahmifj.github.io</code>. Because of this, clicking most links on the custom domain sends users back to GH Pages.</p>
<p>To fix that, I need to make an adjustment on the Worker to rewrite all the GitHub Pages URLs and replace them with the custom domain. With the help of ChatGPT, I also add a condition to:</p>
<ul>
<li>Block search engines indexing bot.</li>
<li>Return 404 for the sitemap.</li>
<li>Remove canonical link before the contents are served on the custom domain.</li>
</ul>
<figure class="highlight">
    <figcaption>
        <span class="hl-filename">worker.js</span>
    </figcaption>
    <pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">async</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">upstream</span> <span class="o">=</span> <span class="s2">&#34;fahmifj.github.io&#34;</span><span class="p">;</span> <span class="c1">// GitHub Pages domain
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">mirror</span> <span class="o">=</span> <span class="s2">&#34;fahmifj.space&#34;</span><span class="p">;</span> <span class="c1">// Custom domain
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Prevent indexing by googlebot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">ua</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;user-agent&#34;</span><span class="p">)</span><span class="o">?</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">bots</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;googlebot&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;bingbot&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;slurp&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;duckduckbot&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;baiduspider&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;yandex&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">bots</span><span class="p">.</span><span class="nx">some</span><span class="p">(</span><span class="nx">bot</span> <span class="p">=&gt;</span> <span class="nx">ua</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">bot</span><span class="p">)))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="s2">&#34;Not for indexing&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">403</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Do not serve sitemap
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span> <span class="o">===</span> <span class="s2">&#34;/sitemap.xml&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">404</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">url</span><span class="p">.</span><span class="nx">hostname</span> <span class="o">=</span> <span class="nx">upstream</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// Read HTML response
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">upstreamResponse</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">headers</span><span class="o">:</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">redirect</span><span class="o">:</span> <span class="s2">&#34;follow&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">contentType</span> <span class="o">=</span> <span class="nx">upstreamResponse</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;content-type&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">contentType</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="s2">&#34;text/html&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">upstreamResponse</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Add no-index header (no crawling on my custom domain)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">(</span><span class="nx">upstreamResponse</span><span class="p">.</span><span class="nx">headers</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">headers</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s2">&#34;X-Robots-Tag&#34;</span><span class="p">,</span> <span class="s2">&#34;noindex, nofollow&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">//  Rewrite all URLs from upstream → custom domain
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Rewrite links
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;a[href]&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">element</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rewriteAttr</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="nx">upstream</span><span class="p">,</span> <span class="nx">mirror</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Rewrite assets
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;img[src], script[src], link[href]&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">element</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rewriteAttr</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="s2">&#34;src&#34;</span><span class="p">,</span> <span class="nx">upstream</span><span class="p">,</span> <span class="nx">mirror</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rewriteAttr</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="nx">upstream</span><span class="p">,</span> <span class="nx">mirror</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Remove canonical tag
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;link[rel=&#39;canonical&#39;]&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">element</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">el</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">upstreamResponse</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">status</span><span class="o">:</span> <span class="nx">upstreamResponse</span><span class="p">.</span><span class="nx">status</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span>
</span></span><span class="line"><span class="cl">      <span class="p">}));</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* ---- Helper ---- */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">rewriteAttr</span><span class="p">(</span><span class="nx">element</span><span class="p">,</span> <span class="nx">attr</span><span class="p">,</span> <span class="nx">upstream</span><span class="p">,</span> <span class="nx">mirror</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="nx">attr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">value</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">value</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">upstream</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nx">attr</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">value</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sb">`https://</span><span class="si">${</span><span class="nx">upstream</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="sb">`https://</span><span class="si">${</span><span class="nx">mirror</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</figure><h2 id="conclusion">Conclusion</h2>
<p>With this setup, the custom domain acting as a mirror can coexist with GH Pages.</p>
<p>You might think that it is odd to keep using GitHub pages even though I already have a custom domain. The reason is simple: I want my blog to stay online as long as possible by relying on a free service provided by GitHub itself.</p>
<p>If I used my custom domain as the primary site, I would be responsible for maintaining that domain (or even server). The custom domain can expire in one or two year and I might forget renewing it or even I decide to switch to another TLD. These all could negatively impact SEO*.</p>
<blockquote>
<p>*or .. maybe I&rsquo;m just a lazy sysadmin.</p>
</blockquote>
<p>But with GitHub Pages as the primary site, I don&rsquo;t have to worry about maintaining a domain. GitHub handles everything and the canonical URL stays consistent. This way, I will still have the freedom to change my custom domain in the future without worrying about SEO.</p>
<p>Okay that&rsquo;s it. See you in the next post!</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
