<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cinnabar Notes</title><description>Just a simple note</description><link>https://notes.cnbr.jp/</link><language>en</language><item><title>Building a Gateway VPS with OPNsense: Private IP, HAProxy, and WireGuard</title><link>https://notes.cnbr.jp/posts/building-a-gateway-vps-with-opnsense/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/building-a-gateway-vps-with-opnsense/</guid><description>How I set up a FreeBSD-based gateway VPS using OPNsense to manage routing, firewall, VPN, and reverse proxy for my other servers.</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
This post is about how I built a gateway VPS using &lt;code&gt;OPNsense&lt;/code&gt;. This article does not cover technical details step-by-step but more about my experience and lessons learned. I hope you find it interesting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’ve always had a &lt;strong&gt;weird bias&lt;/strong&gt; about &lt;code&gt;FreeBSD&lt;/code&gt; vs &lt;code&gt;Linux&lt;/code&gt; for server use.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FreeBSD just &lt;em&gt;feels&lt;/em&gt; better, especially the TCP/IP stack.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux is great, but FreeBSD networking feels… like Ferrari&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anyway, I have &lt;strong&gt;three VPS&lt;/strong&gt; on the same provider. Two of them are &lt;strong&gt;high spec (for me)&lt;/strong&gt; and already running Linux doing real work. The third one is low spec and basically doing nothing. My provider also supports &lt;strong&gt;private virtual IP&lt;/strong&gt; between VPS instances, so I had a cursed idea:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What if the idle VPS becomes the &lt;code&gt;gate&lt;/code&gt; of everything?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Like a small &lt;strong&gt;edge router&lt;/strong&gt; VPS that handles routing/firewall/VPN/reverse proxy, while the bigger Linux boxes just focus on workloads.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                           (Provider Private Network: 10.20.0.0/16)
        ┌───────────────────────────────────────────────────────────────────┐
        │                                                                   │
        │   [OPNsense VPS] 10.20.0.1                                        │
        │       |                                                           │
        │       |---&amp;gt; [Linux Server 1] 10.20.0.11  ---&amp;gt; (Services)          │
        │       |                                                           │
        │       |---&amp;gt; [Linux Server 2] 10.20.0.12  ---&amp;gt; (Services)          │
        │       |                                                           │
        │       |                                                           │
        │       └── WireGuard: wg0 10.60.0.1/24                             │
        │                    (Tunnel Network: 10.60.0.0/24)                 │
        └───────────────────────────────┬───────────────────────────────────┘
                                        │
                                        │  WireGuard tunnel (10.60.0.0/24)
                                        │
                                wg peer │ 10.60.0.2/24
                              ┌─────────▼─────────┐
                              │   [MikroTik]      │
                              │   LAN GW:         │
                              │   192.168.60.1/24 │
                              └─────────┬─────────┘
                                        │
                                        │ (Homelab LAN: 192.168.60.0/24)
                         ┌──────────────┴────────────────┐
                         │                               │
                 [Proxmox Host]                   [VMs on Proxmox]
               192.168.60.10                      192.168.60.101
                                                  192.168.60.102
                                                  ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I went down the FreeBSD firewall rabbit hole.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
My configuration has another trade-off: this gateway VPS is a single point of failure. If it goes down, everything goes down with it: reverse proxy, VPN, routing, the whole “gate of everything.” In my case, it’s acceptable because this is a homelab setup and I can afford some downtime. But in production, consider high availability (HA) setups.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;OPNsense vs pfSense: I picked OPNsense for a lazy reason&lt;/h2&gt;
&lt;p&gt;I knew about &lt;a href=&quot;https://www.pfsense.org/&quot;&gt;pfSense&lt;/a&gt;/&lt;a href=&quot;https://OPNsense.org/&quot;&gt;OPNsense&lt;/a&gt; since my undergrad era, my friend, &lt;a href=&quot;https://www.linkedin.com/in/ahmad-mishbahuddin/&quot;&gt;Ahmad&lt;/a&gt;, was comparing router OS performance (&lt;code&gt;pfSense&lt;/code&gt;, &lt;code&gt;OpenWrt&lt;/code&gt;, etc.) for his thesis while I was benchmarking an Ethereum private network. We published our papers, &lt;a href=&quot;https://jutif.if.unsoed.ac.id/index.php/jurnal/article/view/4047&quot;&gt;Ahmad&apos;s paper&lt;/a&gt; and &lt;a href=&quot;https://beei.org/index.php/EEI/article/view/9219&quot;&gt;my paper here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now the funniest part is that I actually needed one of them.&lt;/p&gt;
&lt;p&gt;When choosing &lt;code&gt;OPNsense vs pfSense&lt;/code&gt;, I went with &lt;code&gt;OPNsense&lt;/code&gt; (sorry Ahmad). Either because I did a deep philosophical comparison nor because I’m a firewall guru.&lt;/p&gt;
&lt;p&gt;My brain literally went:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OPNsense is a pfSense fork → &lt;code&gt;okay I’ll use OPNsense&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s it. &lt;strong&gt;Zero drama&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;opnsense-dashboard.png&quot; alt=&quot;OPNsense Dashboard&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Easy Installation but Harder Access for First Time&lt;/h2&gt;
&lt;p&gt;OPNsense installation is chill, feels like installing FreeBSD. Then you reboot, you configure interfaces, you feel smart for 3 minutes and then the web UI is &lt;strong&gt;not accessible&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I had two NICs. I assigned WAN/LAN. Done. Then I tried to open the Web UI and… nope. I repeating the process a few times, checking IPs, interfaces, everything. Still no access.&lt;/p&gt;
&lt;p&gt;At that moment I learned the classic lesson:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OPNsense Web UI is basically &lt;code&gt;LAN-only&lt;/code&gt; by default and the firewall is &lt;em&gt;not here to be your friend&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As a newbie, this combo is deadly. Because you can easily lock yourself out and then you start questioning life. Also I tried to &lt;strong&gt;just edit the config file like in Linux.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Spoiler: I didn’t even know where the web UI config file was. And even if I found it, I wasn’t confident editing it manually because it’s not that kind of system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So yeah. First time OPNsense experience: &lt;strong&gt;I blocked myself&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;My solution was very professional:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;pfctl -d &lt;/code&gt; → finish setup in Web UI → &lt;code&gt;pfctl -e&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, I temporarily &lt;strong&gt;turned off the firewall on a firewall OS, so I could configure the firewall&lt;/strong&gt;. A peak engineering, but it worked, and after that I could proceed normally and tighten rules properly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Always have console access to your OPNsense box. If you mess up the firewall rules, you can always fix it from the console.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;WireGuard: I suffered on FreeBSD… until OPNsense fixed my misunderstanding&lt;/h2&gt;
&lt;p&gt;Now the fun part: VPN.&lt;/p&gt;
&lt;p&gt;I have a &lt;code&gt;MikroTik router&lt;/code&gt; in my homelab managing all my internal VMs. Since MikroTik supports &lt;code&gt;WireGuard&lt;/code&gt; now, I &lt;em&gt;knew&lt;/em&gt; I could tunnel my homelab to the internet somehow.
&lt;img src=&quot;wireguard-mikrotik.png&quot; alt=&quot;WireGuard Mikrotik&quot; /&gt;
But here’s the thing: for a long time I had a &lt;strong&gt;wrong mental model&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I thought WireGuard “server” means you need &lt;strong&gt;an extra dedicated server&lt;/strong&gt; just for tunneling. Like a separate box that only exists to accept connections and forward traffic. That idea alone already felt like &lt;em&gt;more infra, more pain, more Docker&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So I kept avoiding the &lt;strong&gt;proper&lt;/strong&gt; setup and relied on &lt;a href=&quot;https://github.com/wg-easy/wg-easy&quot;&gt;wg-easy&lt;/a&gt; because it was simple until I remembered: &lt;code&gt;wg-easy&lt;/code&gt; = Docker again. &lt;strong&gt;Oh no.. Docker again.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then I tried WireGuard inside OPNsense and suddenly it clicked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I don’t need an additional tunneling server. &lt;strong&gt;OPNsense &lt;em&gt;is&lt;/em&gt; the gateway, so it can be the WireGuard server too.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;wireguard-opnsense.png&quot; alt=&quot;WireGuard OPNsense&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once I understood the &lt;strong&gt;peer&lt;/strong&gt; concept, everything made way more sense. WireGuard isn’t really “server vs client” the way I used to imagine it, it&apos;s basically &lt;strong&gt;peers exchanging keys&lt;/strong&gt;, and one of them just happens to be the “hub” in my topology. After that, the setup felt weirdly smooth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generate keys&lt;/li&gt;
&lt;li&gt;define peers&lt;/li&gt;
&lt;li&gt;set allowed IPs properly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The setup is smooth, but another challenge appeared: &lt;strong&gt;peers cannot ping&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;rules-dropdown.png&quot; alt=&quot;Rules Dropdown&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As a noob, I thought that &lt;code&gt;WireGuard (Group)&lt;/code&gt; is a rule for the WireGuard interface. But, I was wrong. First you should create interface first, so WireGuard real rules will appear under the correct interface (in my case, &lt;code&gt;WG&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;interfaces-assignments.png&quot; alt=&quot;Interfaces Assignments&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That part confused me for a while, but after I understood it, everything worked fine.&lt;/p&gt;
&lt;h2&gt;HAProxy: friendship ended with Nginx Proxy Manager&lt;/h2&gt;
&lt;p&gt;Next, reverse proxy.&lt;/p&gt;
&lt;p&gt;Before this, I used &lt;a href=&quot;https://nginxproxymanager.com/&quot;&gt;Nginx Proxy Manager&lt;/a&gt; a lot because it’s easy: click-click-done, certificates, hosts, whatever. But it’s Docker-based and my gateway VPS was supposed to be &lt;code&gt;clean&lt;/code&gt; and &lt;code&gt;network-focused&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then I noticed OPNsense can use &lt;a href=&quot;https://www.haproxy.org/&quot;&gt;HAProxy&lt;/a&gt; (and some people also use &lt;code&gt;NGINX&lt;/code&gt;). I read a bit and my brain liked HAProxy because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s very strong for TCP (Layer 4)&lt;/li&gt;
&lt;li&gt;Load balancing feels &lt;strong&gt;native&lt;/strong&gt; to it&lt;/li&gt;
&lt;li&gt;It matches the &lt;strong&gt;gateway&lt;/strong&gt; role better (for me)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;haproxy-main-menu.png&quot; alt=&quot;HAProxy Main Menu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NGINX&lt;/code&gt; is amazing, but &lt;code&gt;HAProxy&lt;/code&gt; felt like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I exist to move traffic efficiently.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So yeah:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Friendship ended with NPM. HAProxy is my best friend now.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;…&lt;strong&gt;but also the friend that keeps giving you migraines.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because unlike NPM where you &lt;strong&gt;just map domain → IP:port&lt;/strong&gt;, HAProxy wants you to understand &lt;strong&gt;its whole worldview&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Frontends&lt;/code&gt; (where traffic enters)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Backends&lt;/code&gt; (where traffic exits)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Backend pools&lt;/code&gt; (multiple servers behind a backend)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Health checks&lt;/code&gt; (because HAProxy refuses to send traffic to something it thinks is dead)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Below here is comparison diagrams of both setups for two services: &lt;a href=&quot;https://memo.cnbr.jp&quot;&gt;memo.cnbr.jp&lt;/a&gt; (hosted on Linux Server 1) and &lt;a href=&quot;https://jellyfin.cnbr.jp&quot;&gt;jellyfin.cnbr.jp&lt;/a&gt; (hosted on a VM in my homelab via WireGuard) if I used Nginx Proxy Manager vs HAProxy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Nginx Proxy Manager

                        ┌─────────────────────────────┐
Internet / Clients ---&amp;gt; │ NPM (Docker) :443 / :80     │
 HTTPS SNI/Host         └──────────────┬──────────────┘
                                       |
                          Proxy Hosts (hostname routing)
                                       |
          +----------------------------+--------------------------------+
          |                                                             |
          v                                                             v
┌───────────────────────┐                                      ┌───────────────────────┐
│ memo.cnbr.jp          │                                      │ jellyfin.cnbr.jp      │
│ Forward to:           │                                      │ Forward to:           │
│ 10.20.0.11:3000       │                                      │ 192.168.60.101:8096   │
│ SSL: ON (Let&apos;s Encrypt│                                      │ SSL: ON (Let&apos;s Encrypt│
│ Health check: (lol)   │                                      │ Health check: (lol)   │
└───────────────────────┘                                      └───────────────────────┘


========================================================================================

HAProxy

                           ┌──────────────────────────────────┐
Internet / Clients  HTTPS  │ OPNsense HAProxy Frontend :443   │
 SNI/Host routing --------&amp;gt;│ Public Service &quot;https-443&quot;       │
                           └───────────────┬──────────────────┘
                                           |
                                   Rules (ordered)
                                           |
                 +-------------------------+--------------------------+
                 |                                                    |
                 v                                                    v
       Condition: hdr(host)=memo.cnbr.jp                    Condition: hdr(host)=jellyfin.cnbr.jp
                 |                                                    |
                 v                                                    v
        Action: use backend pool &quot;bk_memo&quot;                 Action: use backend pool &quot;bk_jellyfin&quot;
                 |                                                    |
                 v                                                    v
     Backend Pool &quot;bk_memo&quot; (10.20.0.0/16)              Backend Pool &quot;bk_jellyfin&quot; (via WG)
     server: 10.20.0.11:3000  (check?)                  server: 192.168.60.101:8096 (check?)
                 |                                                    |
                 v                                                    v
            (Services)                                           (Jellyfin VM)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that last part… the &lt;code&gt;health checks&lt;/code&gt;… is where my sanity started leaking.&lt;/p&gt;
&lt;p&gt;The most evil moment is when everything looks fine, DNS is correct, certificates are okay, your service is running, but your browser says:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;503-service-unavailable.png&quot; alt=&quot;503 Service Unavailable&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At one point, my &lt;code&gt;Jellyfin&lt;/code&gt; suddenly wasn’t reachable. And because my setup involves &lt;code&gt;OPNsense&lt;/code&gt; + &lt;code&gt;HAProxy&lt;/code&gt; + &lt;code&gt;WireGuard&lt;/code&gt; + &lt;code&gt;MikroTik&lt;/code&gt; + &lt;code&gt;firewall rules&lt;/code&gt;, my brain immediately went full paranoia mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Did I break the WireGuard tunnel?”&lt;/li&gt;
&lt;li&gt;“Is MikroTik routing wrong?”&lt;/li&gt;
&lt;li&gt;“Is OPNsense blocking traffic?”&lt;/li&gt;
&lt;li&gt;“Did I mess up NAT or firewall rules?”&lt;/li&gt;
&lt;li&gt;“Is the private IP path dead?”&lt;/li&gt;
&lt;li&gt;“Is my VPS provider doing something weird?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I started checking everything layer by layer like a confused network detective. Then… after way too long… I found the real reason:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HAProxy marked the Jellyfin backend as &lt;strong&gt;DOWN&lt;/strong&gt; not because Jellyfin was dead but because the &lt;code&gt;health check&lt;/code&gt; failed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So HAProxy just refused to forward traffic and gave me &lt;code&gt;503&lt;/code&gt;, even though Jellyfin itself was actually fine. That moment was hilarious because I was debugging &lt;code&gt;WireGuard + MikroTik + firewall&lt;/code&gt; like it was a deep network failure and the problem was literally:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Health check says no.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since then I adopted my professional HAProxy workflow&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Make the HAProxy route work first, &lt;strong&gt;skip health checks at the beginning&lt;/strong&gt;, then enable health checks later&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So yeah, that’s my experience building a gateway VPS with OPNsense.
I’m still learning a lot, but so far it’s been fun.&lt;/p&gt;
&lt;p&gt;My final setup looks great because it&apos;s cleanly separated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OPNsense handles routing, firewall, VPN, and reverse proxy&lt;/li&gt;
&lt;li&gt;Linux servers focus on workloads without worrying about networking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Running Docker on FreeBSD</title><link>https://notes.cnbr.jp/posts/docker-on-freebsd/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/docker-on-freebsd/</guid><description>How I run Docker on FreeBSD.</description><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
This post is about how I run Docker on FreeBSD. I hope you find it interesting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As a DevOps engineer, I often use &lt;code&gt;Docker&lt;/code&gt; to containerize applications. Docker is a great tool for managing containers, and it makes it easy to deploy applications in a consistent and reproducible way. I have been using Docker for a long time, and I am very familiar with it.&lt;/p&gt;
&lt;p&gt;Before &lt;a href=&quot;https://github.com/icaksh/freebsd-nginx-proxy-manager&quot;&gt;I porting Nginx Proxy Manager to run on FreeBSD&lt;/a&gt;, I have to run it on Docker. I use it to manage my reverse proxies, and it makes it easy to set up SSL certificates and manage multiple domains. I have been using it on Linux, and I wanted to see if I could run it on FreeBSD. The bad news is &lt;code&gt;Nginx Proxy Manager&lt;/code&gt; runs on Docker, and FreeBSD does not support Docker natively. So, I must find a way to run Docker on FreeBSD.&lt;/p&gt;
&lt;h2&gt;Running Docker on FreeBSD&lt;/h2&gt;
&lt;p&gt;The best way to run Docker on FreeBSD is to use a Linux virtual machine. I use &lt;code&gt;bhyve&lt;/code&gt; as a hypervisor to run a Linux virtual machine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------+          +------------+          +----------------+
| FreeBSD   | -------- | bhyve      | -------- | Fedora CoreOS  |
+-----------+          +------------+          +----------------+
                                                        |
                                               +----------------+
                                               | Docker Engine  |
                                               +----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The architecture is simple. I run a &lt;code&gt;Fedora CoreOS&lt;/code&gt; virtual machine on &lt;code&gt;bhyve&lt;/code&gt;, and I install &lt;code&gt;Docker Engine&lt;/code&gt; on it. For easier installation, I use &lt;code&gt;vm-bhyve&lt;/code&gt; to manage the virtual machine. Why Fedora CoreOS? Because it&apos;s designed for running containerized workloads. It&apos;s optimized for running containers, and it comes with Docker Engine pre-installed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------+          +------------------------+          +----------------+
| User      | -------- | Host                   | -------- | Fedora CoreOS  |
| 10.42.1.1 |          | 172.16.0.1:40000-50000 |          | 192.168.10.1:x |
+-----------+          +------------------------+          +----------------+

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my VPS, I only have 1 IPv4 address, so I allocate ports &lt;code&gt;40000-50000&lt;/code&gt; from the host to the virtual machine. I use &lt;code&gt;pf&lt;/code&gt; to configure port forwarding. Then, in Docker, I only port forward to the ports in range &lt;code&gt;40000-50000&lt;/code&gt; in the virtual machine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fedora-coreos-on-bhyve.jpg&quot; alt=&quot;Fedora CoreOS on bhyve&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Managing Docker Containers&lt;/h2&gt;
&lt;p&gt;Originally, to manage the Docker containers, we must access the virtual machine with &lt;code&gt;ssh&lt;/code&gt; or &lt;code&gt;vnc&lt;/code&gt;. For easier management, I use &lt;code&gt;portainer&lt;/code&gt; to manage Docker containers. Portainer is a lightweight management UI that allows you to easily manage your Docker containers, images, networks, and volumes. It&apos;s a great tool for managing Docker, and it makes it easy to deploy and manage containers. For testing, I run &lt;code&gt;Ubuntu&lt;/code&gt; with &lt;code&gt;XFCE4&lt;/code&gt; desktop environment on Docker including &lt;code&gt;xrdp&lt;/code&gt; for remote desktop and here is the result.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ubuntu-on-docker-on-freebsd.jpg&quot; alt=&quot;Ubuntu on Docker on Fedora CoreOS on FreeBSD&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------+---------+-----------+---------+-----------+
| FreeBSD   | Bhyve   | Fedora    | Docker  | Ubuntu    |
+-----------+---------+-----------+---------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Known Bug&lt;/h2&gt;
&lt;p&gt;I have a problem with &lt;code&gt;bhyve&lt;/code&gt; that the internet connection is too slow. The guest os always got terrible network internet speed (average in 2 Mbps) when the host internet speed average is 1 Gbps. I have tried using debian, alpine, and fedora coreos and it always got terrible network speed.&lt;/p&gt;
&lt;p&gt;The solution is to disable &lt;code&gt;tcp segmentation offload&lt;/code&gt; and &lt;code&gt;tcp checksum offload&lt;/code&gt; in the host os. Disabling that in the host os will fix the problem, but will make the host speed is not maximum to max (fast enough). Here is the command to disable it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig xxx -txcsum -rxcsum -tso -lero
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That solution I found in &lt;a href=&quot;https://forums.freebsd.org/threads/bhyve-network-performance-is-horrible.93328/&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Additional Notes&lt;/h2&gt;
&lt;p&gt;Running Docker on FreeBSD is not as straightforward as running it on Linux. But, with a little bit of effort, you can run Docker on FreeBSD using a Linux virtual machine. I have been using this setup for a while, and it works well for me. I can run Docker containers on FreeBSD and manage them easily using Portainer.&lt;/p&gt;
&lt;p&gt;This setup is recommended for running Docker on FreeBSD? I can say, NO. I prefer to buy another VPS with Linux installed. That&apos;s the best way to run Docker. But, if you have a VPS with FreeBSD installed, you can use this setup to run Docker.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>My Journey with FreeBSD</title><link>https://notes.cnbr.jp/posts/my-journey-with-freebsd/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/my-journey-with-freebsd/</guid><description>How I started using FreeBSD and my experience with it.</description><pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post is about my journey with FreeBSD, how I started using it, and my experience with it. I hope you find it interesting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I first using FreeBSD in 2021, or perhaps even earlier, while experimenting on DigitalOcean in 2015/2016. At that time (2021), I used Google Cloud Engine as a platform to try it out. I cannot recall the exact date, but I distinctly remember using FreeBSD 12 back then. I eventually &lt;strong&gt;gave up&lt;/strong&gt; due to the configuration files being different from those in Linux (which I will discuss later).&lt;/p&gt;
&lt;h2&gt;Why FreeBSD?&lt;/h2&gt;
&lt;p&gt;I remember when Debian deciding to use systemd as the default init system. I was confused and didn&apos;t like it. When Ubuntu forced me to use snaps, I was also confused. I wanted to use an operating system that respects my freedom and doesn&apos;t force me to use something I don&apos;t like. Surely, I hate that kind of thing. In an article, I have read that 10,000 lines code in systemd is just for init when in FreeBSD it&apos;s make a complete system. I was interested in that. FreeBSD pays great attention to the optimization of its code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;unix-architecture.png&quot; alt=&quot;UNIX Architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I have always been interested in UNIX-like operating systems. I have used Linux for a long time, and I wanted to try something different. I have heard a lot of good things about FreeBSD, such as its stability, security, and performance. I also wanted to learn more about the BSD family of operating systems. I also read an article that FreeBSD is more stable than Linux, and it is used in many critical systems. I was intrigued by the idea of using an operating system that is known for its reliability and security. I decided to give it a try and see how it compares to Linux.&lt;/p&gt;
&lt;h2&gt;My Experience&lt;/h2&gt;
&lt;p&gt;When I first started using FreeBSD, I was impressed by how easy it was to install. The installation process was straightforward, and I had no issues setting up my system. I was able to install all the software I needed using the package manager. But, I was a bit stressed because this operating system is different from Linux. I remember that I had to overnight to make touchpad, webcam, sound, and other things work. That&apos;s was a bit frustrating. I had to learn about ACPI, devd, and other things that I didn&apos;t know before, loading a kernel module, and so on.&lt;/p&gt;
&lt;p&gt;I also had to get used to the different directory structure and file system layout. But, I like the structure of the FreeBSD system. I love the &lt;code&gt;/usr/local/&lt;/code&gt; directory, which is used for installing software from ports. I also like the &lt;code&gt;/etc/rc.conf&lt;/code&gt; file, which is used for system configuration. I found it easier to manage system configuration files in FreeBSD than in Linux.&lt;/p&gt;
&lt;p&gt;FreeBSD Handbook is a great resource for learning about FreeBSD. I have learned a lot from it. I also found the FreeBSD forums and mailing lists to be helpful and I love their jokes. The article from &lt;a href=&quot;https://vermaden.wordpress.com/&quot;&gt;vermaden&lt;/a&gt; is also a great resource for learning about FreeBSD. I have learned a lot from his blog.&lt;/p&gt;
&lt;p&gt;I also create a review of FreeBSD on my Youtube channel. But, it&apos;s in Indonesian. You can watch it.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/Zwr-1OoGzGM&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;What I Love About FreeBSD&lt;/h2&gt;
&lt;p&gt;FreeBSD is a &lt;code&gt;real UNIX&lt;/code&gt; operating system. It is based on the original UNIX codebase and is POSIX compliant. What I mean here is that FreeBSD already encompasses the entirety of an operating system&apos;s needs. We know that Linux is essentially just a kernel, which is the core of an entire operating system. Whereas FreeBSD includes the entire operating system itself. So, FreeBSD is a complete operating system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FreeBSD is &lt;code&gt;UNIX-like&lt;/code&gt; because it is not certified by The Open Group as a UNIX operating system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As someone who has experienced the stability of CentOS, I find FreeBSD to be even more stable. I remember when I use Debian 7, it&apos;s not stable like CentOS 6. I actually like Linux, but the frequently changing kernel versions make me uneasy. Additionally, while systemd has been helpful to me, it doesn&apos;t seem suitable for my cheapest, minimalist, and poor servers. Moreover, because FreeBSD is a real operating system, it is free from third-party interests that might have their own agendas. I still remember when the Linux community split into two: those who supported systemd and those who did not.&lt;/p&gt;
&lt;p&gt;As someone who has used Linux, I feel that there are too many distributions. Many distributions, many tutorials, many documentations, many package managers, many configurations, many everything. I don&apos;t mind having many distributions, but please, come to an agreement to unify the configuration files. Especially now, there are distributions that store configuration files in different paths, which is quite confusing.&lt;/p&gt;
&lt;p&gt;FreeBSD is essentially a community consisting of many people. Within FreeBSD, there is a core team made up of 10 individuals chosen by the community. This core team decides the direction of FreeBSD&apos;s development. Therefore, FreeBSD does not belong to a single person or company, but to the community.&lt;/p&gt;
&lt;p&gt;Why democratic? Every commit to the Linux kernel must be approved by Linus Torvalds. This is not the case with FreeBSD. The core team is responsible for approving commits. This is a good thing because it means that the development of FreeBSD is not dependent on a single person. If Linus Torvalds is gone, who will approve the commits? This is a question that has been asked many times.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I still use Linux for certain purposes, such as in DevOps (docker). It must be acknowledged that Linux has advanced further than FreeBSD, especially in terms of hardware support and ease of use. I sometimes still grumble because &lt;strong&gt;ath10k&lt;/strong&gt; still doesn&apos;t have a driver.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Now What?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;my-freebsd-server.png&quot; alt=&quot;My FreeBSD Server&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Today, when I write that article, I use FreeBSD 14.2-RELEASE on my local NAS storage and my VPS. I have been using FreeBSD for a long time, and I have no plans to switch back to Linux except for AI and DevOps. I am happy with FreeBSD, and I have learned a lot from using it (even if I got a headache).&lt;/p&gt;
&lt;p&gt;For my daily driver, I use MacOS for its simplicity and ease of use. In fact, MacOS contains a little bit of FreeBSD code. So, I can say that I am still using FreeBSD, but in a different form 😄.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want to be a FreeBSD contributor, but I still have a lot to learn. I hope that I can contribute to the FreeBSD community in the future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Cheap Homelab Using Dell Optiplex 3050</title><link>https://notes.cnbr.jp/posts/cheap-nas-local-storage/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/cheap-nas-local-storage/</guid><description>How I build a cheap homelab using Dell Optiplex 3050.</description><pubDate>Thu, 13 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have been using a &lt;code&gt;ZTE B860H&lt;/code&gt; (ARM based) for a long time as my Homelab. It&apos;s a great device, cheap and less power consumption. I have installed Kodi on it and use it as a media player, Jellyfin for streaming, and uTorrent for downloading. I want to say that ZTE B860H is good, but the main issue is the device encoding performance. It&apos;s not good for transcoding. So, I decided to build a new Homelab.&lt;/p&gt;
&lt;p&gt;I have been thinking about building a cheap Homelab using a PC. My main PC is &lt;code&gt;HP EliteDesk 800 G4 SFF&lt;/code&gt; that contains an Intel Core i7-8700, 16GB RAM, and 256GB SSD. I have a plan to use it as a NAS storage, after I bought a new PC. But, the build is too good and I love it (EliteDesk bro). I don&apos;t want to use it as a Homelab. So, I have been looking for a cheap PC for Homelab.&lt;/p&gt;
&lt;h2&gt;Finding a PC&lt;/h2&gt;
&lt;p&gt;I have been looking for a cheap PC for Homelab. I have a plan to buy a new PC, but I don&apos;t have enough money. I have been looking for a PC on the internet, and I found a &lt;code&gt;Dell Optiplex 3050&lt;/code&gt;. It&apos;s a small form factor PC that contains an Intel Core i3-7100T, 8GB RAM, and 256GB SATA SSD. The price is around 70 USD (secondhand). I think it&apos;s a good deal, so I bought it without SSD. I got the real price is 55 USD.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;umis-128gb.jpg&quot; alt=&quot;UMIS 128GB&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Dell Optiplex 3050 only have 1x 2.5&quot; SATA slot. This is a bit of a problem because the main operating system must be installed in separate storage. I have a plan to use a 1TB HDD that I have for storage. So, I bought a cheap secondhand 128GB NVME SSD for the main operating system. The price is around 20 USD. The price is around 6.11 USD.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I don&apos;t think about how good the SSD is because it&apos;s just for running the operating system (just buy it). But, luckily the SSD is good. It&apos;s a &lt;code&gt;UMIS 128GB&lt;/code&gt;. I have never heard about this brand before. But, I have read that this SSD used in Lenovo Thinkpad. So, I think it&apos;s good 😁.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Building the Homelab&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;dell-optiplex-3050.jpg&quot; alt=&quot;Dell Optiplex 3050&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I cannot say that I really build a Homelab or not because it just a PC with a 1 SATA HDD and low spec. The system is not RAID implemented.&lt;/p&gt;
&lt;p&gt;For operating system, I use &lt;code&gt;FreeBSD 14.2-RELEASE&lt;/code&gt;. I don&apos;t run Proxmox because high of loss performance and the processor is not good. I make 3 partitions on the 1TB HDD. The first partition is for &lt;code&gt;Media&lt;/code&gt; (anime and movie), the second partition is for &lt;code&gt;Data&lt;/code&gt; (cloud storage), and the third partition is for &lt;code&gt;Secret&lt;/code&gt; (personal files) with just personal files partition encrypted with &lt;code&gt;GELI&lt;/code&gt;. In other hand, the 128GB NVME SSD is used for the operating system. All of the partitions filesystem is &lt;code&gt;ZFS&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The main application are installed inside jail. What I mean here is that I don&apos;t install the application directly on the host. I use &lt;code&gt;bastille&lt;/code&gt; for jail management. I have installed &lt;code&gt;Jellyfin&lt;/code&gt;, &lt;code&gt;Radarr&lt;/code&gt;, &lt;code&gt;Sonarr&lt;/code&gt;, &lt;code&gt;Lidarr&lt;/code&gt; on same jail while I separate &lt;code&gt;Nextcloud&lt;/code&gt; in different jail.&lt;/p&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;jellyfin.jpg&quot; alt=&quot;Jellyfin&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I have installed Jellyfin for streaming. The encoding performance is good than ZTE B860H. The Kodi performance is also good. I can play UHD video without any problem (in ZTE B860H, I must waiting the video encoded first or buffering).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;nextcloud.jpg&quot; alt=&quot;Nextcloud&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I also installed Nextcloud for cloud storage (and I love it). The main problem installing Nextcloud in FreeBSD more complex than in Linux. I have to install &lt;code&gt;PHP&lt;/code&gt;, &lt;code&gt;Postgres&lt;/code&gt;, &lt;code&gt;Nginx&lt;/code&gt;, and &lt;code&gt;Nextcloud&lt;/code&gt; itself including activating the &lt;code&gt;PHP extension&lt;/code&gt; and configuring all of it.&lt;/p&gt;
&lt;p&gt;I dont make my homelab as hosting server. I just use it for personal use. The main problem is internet connection in the dorm I live in is too bad (20 Mbps). For public access, I planned to use wireguard for VPN, connecting to my VPS.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Dell Optiplex 3050 is a good PC for Homelab. The price is cheap, and the performance is good enough for personal use with low power consumption than others. But, the main problem is the storage. The PC only have 1 2.5&quot; SATA slot. For now, big HDD storage like 10 TB only available in 3.5&quot; form factor. I&apos;m sure that HP EliteDesk 800 G4 SFF is better than Dell Optiplex 3050 for homelab server (it contains 3 SATA slot and 2 M.2 slot).&lt;/p&gt;
&lt;p&gt;My homelab build is enough for personal use. I can stream video, and cloud storage. The performance is good enough for me. I am happy with my homelab, and I have learned a lot from using it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>JWT Implementation with Redis on Golang</title><link>https://notes.cnbr.jp/posts/jwt-implementation-with-redis-on-golang/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/jwt-implementation-with-redis-on-golang/</guid><description>Best practice for implementing JWT with Redis on Golang.</description><pubDate>Thu, 14 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For now, JWT (JSON Web Token) is a popular way to authenticate users in web applications. JWT is a token that is used to authenticate users and is passed in the HTTP header. JWT is a self-contained token that contains information about the user. JWT is signed with a secret key, so it is secure and cannot be tampered with. The body of the JWT separated by a dot (&lt;code&gt;.&lt;/code&gt;) contains three parts: header, payload, and signature. The header contains information about the algorithm used to sign the token. The payload contains information about the user. The signature is used to verify the token.&lt;/p&gt;
&lt;p&gt;As far as I know, many developers implement JWT in a unsafe way. They store the JWT in the client-side (e.g., local storage, session storage, or cookie). This is not safe because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If the JWT is stolen, the attacker can impersonate the user.&lt;/li&gt;
&lt;li&gt;If the JWT is stolen, the attacker can access until the JWT is expired.&lt;/li&gt;
&lt;li&gt;JWT can used even if the user has logged out.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;How to solve that problem? We have 2 options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create the token with short expiration time (e.g., 15 minutes).&lt;/li&gt;
&lt;li&gt;Store the token to server-side (e.g., database or cache).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first approach is good, but it has a problem. The user must re-login every 15 minutes. This is not user-friendly. The second approach is better. The token is stored in the server-side, so the user cannot access the token. The problem is server-side must do query to database check the token. This is not efficient. The best practice is to store the token in cache. In this post, I will show you how to implement JWT with Redis in Golang. I use this approach in my projects, and it works well.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Expired&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access Token&lt;/td&gt;
&lt;td&gt;15 menit&lt;/td&gt;
&lt;td&gt;User Access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh Token&lt;/td&gt;
&lt;td&gt;1 bulan&lt;/td&gt;
&lt;td&gt;Refresh Access Token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The token is separated into 2 type: access token and refresh token. The access token is used to authenticate the user. The access token has a short expiration time (e.g., 15 minutes). The refresh token is used to get a new access token. The refresh token has a long expiration time (e.g., 7 days). The refresh token is stored in the Redis cache. When the user logs out, the refresh token is deleted from the Redis cache.&lt;/p&gt;
&lt;p&gt;Why do we need to separate the token into 2 types?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If the access token is expired, the user must re-login. This is not user-friendly.&lt;/li&gt;
&lt;li&gt;If the access token is stolen, the attacker can impersonate the user. The access token has a short expiration time, so the attacker cannot impersonate the user for a long time.&lt;/li&gt;
&lt;li&gt;Logout process is easy. Just delete the refresh token from the Redis cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Backend&lt;/h2&gt;
&lt;p&gt;We need to define the JWT models. We have 3 models: &lt;code&gt;JwtTokenDetails&lt;/code&gt;, &lt;code&gt;JwtAuthModel&lt;/code&gt;, and &lt;code&gt;JwtRtModel&lt;/code&gt;. The &lt;code&gt;JwtTokenDetails&lt;/code&gt; model contains the access token, refresh token, access UUID, refresh UUID, access token expiration time, and refresh token expiration time. The &lt;code&gt;JwtAuthModel&lt;/code&gt; model contains the user ID, email, first name, last name, role, and duration. The &lt;code&gt;JwtRtModel&lt;/code&gt; model contains the user ID, email, role, and duration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JWT Models
package models

import &quot;github.com/google/uuid&quot;

type JwtTokenDetails struct {
	AccessToken  string
	RefreshToken string
	AccessUuid   string
	RefreshUuid  string
	AtExpires    int64
	RtExpires    int64
}

type JwtAuthModel struct {
	AccessId  uuid.UUID
	RefreshId uuid.UUID
	UserId    uuid.UUID
	Email     string
	FirstName string
	LastName  string
	Role      int16
	Duration  int64
}

type JwtRtModel struct {
	AccessId  uuid.UUID
	RefreshId uuid.UUID
	UserId    uuid.UUID
	Email     string
	Role      int16
	Duration  int64
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we create JWT utils for handling generation of JWT. We have 2 functions: &lt;code&gt;GenerateNewAuthToken&lt;/code&gt; and &lt;code&gt;GenerateNewAccessToken&lt;/code&gt;. The &lt;code&gt;GenerateNewAuthToken&lt;/code&gt; function is used to generate a new access token and refresh token. The &lt;code&gt;GenerateNewAccessToken&lt;/code&gt; function is used to generate a new access token. The access token is signed with the secret key, and the refresh token is signed with the refresh key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JWT Utils
package utils

import (
	&quot;github.com/icaksh/cripis/app/models&quot;
	&quot;os&quot;
	&quot;strconv&quot;
	&quot;time&quot;

	&quot;github.com/golang-jwt/jwt&quot;
)

func GenerateNewAuthToken(par *models.JwtAuthModel) (*models.JwtTokenDetails, error) {
	td := &amp;amp;models.JwtTokenDetails{}
	var err error
	minutesCount, _ := strconv.Atoi(os.Getenv(&quot;JWT_ACCESS_TIME_KEY_EXPIRE_MINUTES_COUNT&quot;))
	accessTime := time.Now().Add(time.Minute * time.Duration(minutesCount)).Unix()
	atm := &amp;amp;JWTAccess{
		AccessUuid: par.AccessId,
		User:       par.UserId,
		Role:       par.Role,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: accessTime,
		},
	}
	accessToken, err := GenerateNewAccessToken(atm)

	rtm := &amp;amp;JWTRefresh{
		RefreshUuid: par.RefreshId,
		User:        par.UserId,
		Email:       par.Email,
		FirstName:   par.FirstName,
		LastName:    par.LastName,
		Role:        par.Role,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: par.Duration,
		},
	}
	refreshToken, err := GenerateNewRefreshToken(rtm)

	if err != nil {
		return nil, err
	}

	td = &amp;amp;models.JwtTokenDetails{
		AccessUuid:   par.AccessId.String(),
		AccessToken:  accessToken,
		RefreshUuid:  par.RefreshId.String(),
		RefreshToken: refreshToken,
		AtExpires:    accessTime,
		RtExpires:    par.Duration,
	}
	return td, err
}

func GenerateNewAccessToken(atm *JWTAccess) (string, error) {
	secret := os.Getenv(&quot;JWT_SECRET_KEY&quot;)

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, atm)

	accessToken, err := token.SignedString([]byte(secret))
	if err != nil {
		// Return error, it JWT token generation failed.
		return &quot;&quot;, err
	}
	return accessToken, nil
}

func GenerateNewRefreshToken(rtm *JWTRefresh) (string, error) {
	secret := os.Getenv(&quot;JWT_REFRESH_KEY&quot;)

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, rtm)

	refreshToken, err := token.SignedString([]byte(secret))
	if err != nil {
		return &quot;nil&quot;, err
	}
	return refreshToken, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we create utils for JWT validation and extraction. We have 4 functions: &lt;code&gt;ExtractRefreshToken&lt;/code&gt;, &lt;code&gt;ExtractRefreshTokenMetadata&lt;/code&gt;, &lt;code&gt;ExtractTokenMetadata&lt;/code&gt;, and &lt;code&gt;TokenValid&lt;/code&gt;. The &lt;code&gt;ExtractRefreshToken&lt;/code&gt; function is used to extract the refresh token from the request body. The &lt;code&gt;ExtractRefreshTokenMetadata&lt;/code&gt; function is used to extract the refresh token from the request header. The &lt;code&gt;ExtractTokenMetadata&lt;/code&gt; function is used to extract the access token from the request header. The &lt;code&gt;TokenValid&lt;/code&gt; function is used to check if the access token is valid.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package utils

import (
	&quot;os&quot;
	&quot;strings&quot;

	&quot;github.com/gofiber/fiber/v2&quot;
	&quot;github.com/golang-jwt/jwt&quot;
	&quot;github.com/google/uuid&quot;
)

type JWTAccess struct {
	AccessUuid uuid.UUID `json:&quot;accessUuid&quot;`
	User       uuid.UUID `json:&quot;userId&quot;`
	Role       int16     `json:&quot;role&quot;`
	jwt.StandardClaims
}

type JWTRefresh struct {
	RefreshUuid uuid.UUID `json:&quot;refreshUuid&quot;`
	User        uuid.UUID `json:&quot;userId&quot;`
	FirstName   string    `json:&quot;firstName&quot;`
	LastName    string    `json:&quot;lastName&quot;`
	Email       string    `json:&quot;email&quot;`
	Role        int16     `json:&quot;role&quot;`
	jwt.StandardClaims
}

func ExtractRefreshToken(t string) (*JWTRefresh, error) {
	token, err := jwt.ParseWithClaims(t, &amp;amp;JWTRefresh{}, jwtKeyFunc)
	if err != nil {
		return nil, err
	}
	claims, ok := token.Claims.(*JWTRefresh)
	if ok &amp;amp;&amp;amp; token.Valid {
		return &amp;amp;JWTRefresh{
			RefreshUuid: claims.RefreshUuid,
			User:        claims.User,
			Email:       claims.Email,
			FirstName:   claims.FirstName,
			LastName:    claims.LastName,
			Role:        claims.Role,
			StandardClaims: jwt.StandardClaims{
				ExpiresAt: claims.ExpiresAt,
			},
		}, nil
	}
	return nil, err
}

func ExtractRefreshTokenMetadata(c *fiber.Ctx) (*JWTRefresh, error) {
	token, err := verifyRefreshToken(c)
	if err != nil {
		return nil, err
	}

	// Setting and checking token and credentials.
	claims, ok := token.Claims.(*JWTRefresh)
	if ok &amp;amp;&amp;amp; token.Valid {
		// Expires time.
		return &amp;amp;JWTRefresh{
			RefreshUuid: claims.RefreshUuid,
			User:        claims.User,
			Email:       claims.Email,
			FirstName:   claims.FirstName,
			LastName:    claims.LastName,
			Role:        claims.Role,
			StandardClaims: jwt.StandardClaims{
				ExpiresAt: claims.ExpiresAt,
			},
		}, nil
	}

	return nil, err
}

func ExtractTokenMetadata(c *fiber.Ctx) (*JWTAccess, error) {
	token, err := verifyToken(c)
	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(*JWTAccess)
	if ok &amp;amp;&amp;amp; token.Valid {
		return &amp;amp;JWTAccess{
			AccessUuid: claims.AccessUuid,
			User:       claims.User,
			Role:       claims.Role,
			StandardClaims: jwt.StandardClaims{
				ExpiresAt: claims.ExpiresAt,
			},
		}, nil
	}

	return nil, err
}

func TokenValid(c *fiber.Ctx) error {
	token, err := verifyToken(c)
	if err != nil {
		return err
	}
	if _, ok := token.Claims.(jwt.Claims); !ok &amp;amp;&amp;amp; !token.Valid {
		return err
	}
	return nil
}

func extractToken(c *fiber.Ctx) string {
	bearToken := c.Get(&quot;Authorization&quot;)

	// Normally Authorization HTTP header.
	onlyToken := strings.Split(bearToken, &quot; &quot;)
	if len(onlyToken) == 2 {
		return onlyToken[1]
	}

	return &quot;&quot;
}

func verifyToken(c *fiber.Ctx) (*jwt.Token, error) {
	tokenString := extractToken(c)

	token, err := jwt.ParseWithClaims(tokenString, &amp;amp;JWTAccess{}, jwtKeyFunc)
	if err != nil {
		return nil, err
	}

	return token, nil
}

func verifyRefreshToken(c *fiber.Ctx) (*jwt.Token, error) {
	tokenString := extractToken(c)

	token, err := jwt.ParseWithClaims(tokenString, &amp;amp;JWTRefresh{}, jwtKeyFunc)
	if err != nil {
		return nil, err
	}

	return token, nil
}
func jwtKeyFunc(token *jwt.Token) (interface{}, error) {
	return []byte(os.Getenv(&quot;JWT_SECRET_KEY&quot;)), nil
}
func jwtRefreshKeyFunc(token *jwt.Token) (interface{}, error) {
	return []byte(os.Getenv(&quot;JWT_REFRESH_KEY&quot;)), nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to create the JWT queries for Redis. We use the &lt;code&gt;github.com/go-redis/redis&lt;/code&gt; package to connect to the Redis database. We have 3 functions: &lt;code&gt;CreateAuth&lt;/code&gt;, &lt;code&gt;FetchAuth&lt;/code&gt; and &lt;code&gt;DeleteAuth&lt;/code&gt;. The &lt;code&gt;CreateAuth&lt;/code&gt; function is used to create the refresh token in the Redis cache. The &lt;code&gt;FetchAuth&lt;/code&gt; function is used to get the refresh token from the Redis cache. The &lt;code&gt;DeleteAuth&lt;/code&gt; function is used to delete the refresh token from the Redis cache.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package queries

import (
	&quot;fmt&quot;
	&quot;github.com/go-redis/redis/v7&quot;
	&quot;github.com/google/uuid&quot;
	&quot;github.com/icaksh/cripis/app/models&quot;
	&quot;github.com/icaksh/cripis/app/utils&quot;
	&quot;strconv&quot;
	&quot;time&quot;
)

type JWTQueries struct {
	*redis.Client
}

func (q *JWTQueries) CreateAuth(userId uuid.UUID, td *models.JwtTokenDetails) error {
	fmt.Println(td)
	at := time.Until(time.Unix(td.AtExpires, 0))
	rt := time.Until(time.Unix(td.RtExpires, 0))
	fmt.Println(rt)
	errAccess := q.Set(td.AccessUuid, userId, at).Err()
	if errAccess != nil {
		return errAccess
	}
	errRefresh := q.Set(td.RefreshUuid, userId, rt).Err()
	if errRefresh != nil {
		return errRefresh
	}
	return nil
}

func (q *JWTQueries) FetchAuth(authD *utils.JWTAccess) (uint64, error) {
	userid, err := q.Get(authD.AccessUuid.String()).Result()
	if err != nil {
		return 0, err
	}
	userID, _ := strconv.ParseUint(userid, 10, 64)
	return userID, nil
}

func (q *JWTQueries) DeleteAuth(uuid uuid.UUID) (int64, error) {
	deleted, err := q.Del(uuid.String()).Result()
	if err != nil {
		return 0, err
	}
	return deleted, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After we create the queries, we can make database connection. We have 2 functions: &lt;code&gt;RedisConnection&lt;/code&gt; and &lt;code&gt;RedisConnect&lt;/code&gt;. The &lt;code&gt;RedisConnection&lt;/code&gt; function is used to connect to the Redis database. The &lt;code&gt;RedisConnect&lt;/code&gt; function is used to connect to the Redis database and return the JWT queries.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package database

import (
	&quot;github.com/go-redis/redis/v7&quot;
	&quot;os&quot;
)

type RedisQueries struct {
	*queries.JWTQueries
}

func RedisConnection() (*redis.Client, error) {
	client := redis.NewClient(&amp;amp;redis.Options{
		Username: os.Getenv(&quot;REDIS_SERVER_NAME&quot;),
		Password: os.Getenv(&quot;REDIS_SERVER_PASS&quot;),
		Addr:     os.Getenv(&quot;REDIS_SERVER_URL&quot;),
		DB:       0,
	})
	_, err := client.Ping().Result()
	if err != nil {
		return nil, err
	}
	return client, nil
}

func RedisConnect() (*RedisQueries, error) {
	redis, err := RedisConnection()
	if err != nil {
		return nil, err
	}

	return &amp;amp;RedisQueries{
		JWTQueries: &amp;amp;queries.JWTQueries{Client: redis},
	}, nil

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create the auth controller. We have 2 functions: &lt;code&gt;Login&lt;/code&gt; and &lt;code&gt;Logout&lt;/code&gt;. The &lt;code&gt;Login&lt;/code&gt; function is used to authenticate the user and generate the access token and refresh token. The &lt;code&gt;Logout&lt;/code&gt; function is used to delete the refresh token from the Redis cache.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Auth Controller
package controllers

import (
	&quot;fmt&quot;
	&quot;github.com/google/uuid&quot;
	&quot;github.com/icaksh/cripis/app/models&quot;
	&quot;github.com/icaksh/cripis/app/utils&quot;
	&quot;github.com/icaksh/cripis/platform/database&quot;
	&quot;github.com/thanhpk/randstr&quot;
	&quot;os&quot;
	&quot;strconv&quot;
	&quot;strings&quot;
	&quot;time&quot;

	&quot;github.com/gofiber/fiber/v2&quot;
	&quot;golang.org/x/crypto/bcrypt&quot;
)

func Login(c *fiber.Ctx) error {
	// Login Logic

	hoursCount, _ := strconv.Atoi(os.Getenv(&quot;JWT_REFRESH_TIME_KEY_EXPIRE_HOURS_COUNT&quot;))

	if !creds.Remember {
		hoursCount, _ = strconv.Atoi(os.Getenv(&quot;JWT_REFRESH_TIME_KEY_EXPIRE_HOURS_COUNT_REMEMBER&quot;))
	}
	refreshTime := time.Now().Add(time.Hour * time.Duration(hoursCount)).Unix()
	au := &amp;amp;models.JwtAuthModel{
		AccessId:  uuid.New(),
		RefreshId: uuid.New(),
		UserId:    auth.ID,
		Email:     auth.Email,
		FirstName: auth.FirstName,
		LastName:  auth.LastName,
		Role:      auth.Roles,
		Duration:  refreshTime,
	}
	token, err := utils.GenerateNewAuthToken(au)
	if err != nil {
		return utils.InternalServerError(c, err)
	}

	redisDb, err := database.RedisConnect()
	if err != nil {
		return utils.InternalServerError(c, err)
	}
	refresh := redisDb.CreateAuth(auth.ID, token)
	if refresh != nil {
		return utils.InternalServerError(c, err)
	}
	tokens := map[string]string{
		&quot;access_token&quot;:  token.AccessToken,
		&quot;refresh_token&quot;: token.RefreshToken,
	}
	return c.Status(fiber.StatusCreated).JSON(tokens)
}

func Logout(c *fiber.Ctx) error {

	au, err := utils.ExtractTokenMetadata(c)
	fmt.Println(au.AccessUuid)
	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			&quot;error&quot;:   true,
		})
	}
	redisDb, err := database.RedisConnect()

	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			&quot;error&quot;:   true,
		})
	}

	deleted, err := redisDb.DeleteAuth(au.AccessUuid)
	if err != nil || deleted == 0 {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			&quot;error&quot;:   true,
		})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		&quot;message&quot;: &quot;Berhasil keluar&quot;,
	})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to create the refresh token controller. We have 1 function: &lt;code&gt;RefreshToken&lt;/code&gt;. The &lt;code&gt;RefreshToken&lt;/code&gt; function is used to refresh the access token. The refresh token is sent in the request body, extracted, and then the user ID is fetched from the Redis cache. After that, the access token is generated with the new expiration time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package controllers

import (
	&quot;fmt&quot;
	&quot;github.com/google/uuid&quot;
	&quot;github.com/icaksh/cripis/app/models&quot;
	&quot;github.com/icaksh/cripis/app/utils&quot;
	&quot;github.com/icaksh/cripis/platform/database&quot;
	&quot;github.com/thanhpk/randstr&quot;
	&quot;os&quot;
	&quot;strconv&quot;
	&quot;strings&quot;
	&quot;time&quot;

	&quot;github.com/gofiber/fiber/v2&quot;
	&quot;golang.org/x/crypto/bcrypt&quot;
)

func RefreshToken(c *fiber.Ctx) error {
	body := models.RefreshToken{}
	err := c.BodyParser(&amp;amp;body)

	if err != nil {
		return utils.BadRequest(c, err)
	}

	validate := utils.NewValidator()
	if err := validate.Struct(&amp;amp;body); err != nil {
		return utils.BadRequest(c, err)
	}

	redisDb, err := database.RedisConnect()

	if err != nil {
		return utils.InternalServerError(c, err)
	}

	claims, err := utils.ExtractRefreshToken(body.RefreshToken)
	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			&quot;error&quot;:   true,
			&quot;message&quot;: &quot;Invalid token&quot;,
		})
	}

	deleted, err := redisDb.DeleteAuth(claims.RefreshUuid)

	if err != nil || deleted == 0 {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			&quot;error&quot;:   true,
			&quot;message&quot;: &quot;Invalid token&quot;,
		})
	}

	au := &amp;amp;models.JwtAuthModel{
		AccessId:  uuid.New(),
		RefreshId: uuid.New(),
		UserId:    claims.User,
		Email:     claims.Email,
		FirstName: claims.FirstName,
		LastName:  claims.LastName,
		Role:      claims.Role,
		Duration:  claims.ExpiresAt,
	}

	token, err := utils.GenerateNewAuthToken(au)
	if err != nil {
		return utils.InternalServerError(c, err)
	}

	refresh := redisDb.CreateAuth(claims.User, token)
	if refresh != nil {
		return utils.InternalServerError(c, err)
	}
	tokens := map[string]string{
		&quot;access_token&quot;:  token.AccessToken,
		&quot;refresh_token&quot;: token.RefreshToken,
	}

	return c.Status(fiber.StatusCreated).JSON(tokens)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have finished building the logic we need to generate, validate, and refresh the token. Now we need to create the middleware to protect the route. We have 1 function: &lt;code&gt;JWTProtected&lt;/code&gt;. The &lt;code&gt;JWTProtected&lt;/code&gt; function is used to check if the access token is valid. The access token is sent in the request header, extracted, and then the user ID is fetched from the Redis cache. If the access token is valid, the user is authenticated. If the access token is invalid, the user is not authenticated.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package middleware

import (
	&quot;github.com/gofiber/fiber/v2&quot;
	&quot;github.com/icaksh/cripis/app/utils&quot;
)

func JWTProtected(c *fiber.Ctx) error {
	err := utils.TokenValid(c)
	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			&quot;error&quot;: true,
			&quot;msg&quot;:   err.Error(),
		})
	}
	c.Next()
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Last, in routes, add routes to refresh token and implement middleware to protect the route.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package routes

import (
	&quot;github.com/icaksh/cripis/app/controllers&quot;
	&quot;github.com/icaksh/cripis/pkg/middleware&quot;
	recaptcha &quot;github.com/jansvabik/fiber-recaptcha&quot;

	&quot;github.com/gofiber/fiber/v2&quot;
)

func AuthRoutes(private fiber.Router, public fiber.Router) {
	pathName := &quot;/auth&quot;
	public.Post(pathName+&quot;/login&quot;, recaptcha.Middleware, controllers.Login)
	public.Post(pathName+&quot;/refresh&quot;, controllers.RefreshToken)
	public.Post(pathName+&quot;/register&quot;, recaptcha.Middleware, controllers.Register)
	public.Post(pathName+&quot;/reset&quot;, recaptcha.Middleware, controllers.Reset)
	//
	private.Post(pathName+&quot;/logout&quot;, middleware.JWTProtected, controllers.Logout)
}

func UserRoutes(private fiber.Router, public fiber.Router) {
	pathName := &quot;/user&quot;
	private.Get(pathName+&quot;s&quot;, middleware.JWTProtected, controllers.GetUsers)
	private.Get(pathName+&quot;/&quot;, middleware.JWTProtected, controllers.GetUser)
	private.Get(pathName+&quot;/:id&quot;, middleware.JWTProtected, controllers.GetUserById)
	private.Put(pathName+&quot;/&quot;, middleware.JWTProtected, controllers.EditUser)
	private.Put(pathName+&quot;/password&quot;, middleware.JWTProtected, controllers.EditUserPassword)
	private.Put(pathName+&quot;/status&quot;, middleware.JWTProtected, controllers.EditUserStatus)
	private.Delete(pathName+&quot;/:id&quot;, middleware.JWTProtected, controllers.DeleteUser)
	//
	public.Get(pathName+&quot;/roles&quot;, controllers.GetUserRoles)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Frontend&lt;/h2&gt;
&lt;p&gt;Handling token in frontend is easy. Just store the token in the local storage and configure the Axios to send the token in the header using interceptors when &lt;code&gt;request&lt;/code&gt;. Here is the example code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;axios.interceptors.request.use(
    (config) =&amp;gt; {
        const user = JSON.parse(fastLocalStorage.getItem(&apos;user&apos;));
        if (user &amp;amp;&amp;amp; user.access_token) {
            config.headers.Authorization = `Bearer ${user.access_token}`;
        }
        return config;
    },
    (error) =&amp;gt; {
        return Promise.reject(error);
    }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, the token is sent in the header when the request is sent. If the token is expired, the server will return a 401 status code. We need to handle this error. If the server returns a 401 status code, we need to send a request to the server to get a new access token using the refresh token and send the original request back. Here is the example code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;axios.interceptors.response.use(
    (response) =&amp;gt; {
        return response;
    },
    async (error) =&amp;gt; {
        const originalRequest = error.config;
        if (error.response.status === 401 &amp;amp;&amp;amp; !originalRequest._retry) {
            originalRequest._retry = true;
            try {
                const user = JSON.parse(fastLocalStorage.getItem(&apos;user&apos;));
                const uninterceptedAxiosInstance = axios.create();
                const response = await uninterceptedAxiosInstance.post(process.env.VUE_APP_BACKEND_URL + &apos;/public/auth/refresh&apos;, {
                    refresh_token: user.refresh_token,
                });
                fastLocalStorage.removeItem(&apos;user&apos;)
                fastLocalStorage.setItem(&apos;user&apos;, JSON.stringify(response.data));
                originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
                return axios(originalRequest);
            } catch (refreshError) {
                Vue.$store.dispatch(&apos;logout&apos;);
            }
        }
        return Promise.reject(error);
    }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! Now you have implemented JWT with Redis in Golang. This approach is secure and efficient. The token is stored in the Redis cache, so the user cannot access the token. The refresh token is used to get a new access token. The refresh token has a long expiration time, so the user does not need to re-login frequently. The refresh token is deleted from the Redis cache when the user logs out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
What if the refresh token is stolen? That is a good question. But I don&apos;t know exactly the answer. That&apos;s a risk we have to take. If the refresh token is stolen, the attacker can get a new access token. But the access token has a short expiration time, so the attacker cannot impersonate the user for a long time. The refresh token is deleted from the Redis cache when the user logs out. So, the risk is minimized.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For full implementation, you can check this repository&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;icaksh/cripis&quot;}
::github{repo=&quot;icaksh/cripis-fe&quot;}&lt;/p&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Cloud Run Protection with Cloudflare</title><link>https://notes.cnbr.jp/posts/cloud-run-protection-with-cloudflare/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/cloud-run-protection-with-cloudflare/</guid><description>How I protect Cloud Run with Cloudflare.</description><pubDate>Mon, 12 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have a simple REST API deployed on Cloud Run. The application is simple, just checking daily facts from Wikipedia. Well, since I have a high open-source spirit (wkakakak), I decided to add one feature that I think is not too crucial before I publish the source code. The feature is Check the Latest Earthquake Information. Why did I choose Cloud Run? It&apos;s free up to 2 million requests per month, easy to deploy, cheap (I only pay less than 150 IDR (0.01 USD) per month for Golang apps. However, there is one problem that I encountered.&lt;/p&gt;
&lt;p&gt;In 2023, I got a pretty big DDoS attack. Actually, it&apos;s not a DDoS, but there is a user who accesses the API with a request count per second. Unfortunately, I only realized this when the end of the month and my Cloud Run bill skyrocketed. The plot twist of the attack is that the attack accessed the Latest Earthquake API which is not too crucial.
&lt;img src=&quot;request-attack.png&quot; alt=&quot;Request Attack&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Pointing Domain to Cloud Run&lt;/h2&gt;
&lt;p&gt;Cloud Run by default doesnt have DDoS protection. The original DDoS protection in Google Cloud is Cloud Armor. However, Cloud Armor is not free. I have to pay for the Cloud Armor service. I use Cloudflare as a free DDoS protection service, so I decided to use Cloudflare to protect my Cloud Run.&lt;/p&gt;
&lt;p&gt;First, we need to add domain to Cloudflare.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add domain linked on Cloud Run to Cloudflare.
&lt;img src=&quot;pointing-domain-cloud-run.png&quot; alt=&quot;Pointing Domain Cloud Run&quot; /&gt;&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;Cloudflare DNS&lt;/code&gt;, go to &lt;code&gt;Rules&lt;/code&gt; &amp;gt; &lt;code&gt;Configuration Rules&lt;/code&gt; and &lt;code&gt;+ Create Rules&lt;/code&gt;
&lt;img src=&quot;cloudflare-configuration-rules.png&quot; alt=&quot;CloudFlare Configuration Rules&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Fill the configuration as follows:
&lt;ul&gt;
&lt;li&gt;Fill the Rule Name&lt;/li&gt;
&lt;li&gt;Checklist &lt;code&gt;Custom filter expression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;When incoming requests match&lt;/code&gt; to &lt;code&gt;Hostname&lt;/code&gt; &lt;code&gt;equals&lt;/code&gt; &lt;code&gt;yourdomain.com&lt;/code&gt;
&lt;img src=&quot;cloudflare-configuration-rules-2.png&quot; alt=&quot;CloudFlare Configuration Rules 2&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Then the settings are&lt;/code&gt; to &lt;code&gt;Automatic HTTPS Rewrites&lt;/code&gt; &lt;code&gt;Off&lt;/code&gt;, &lt;code&gt;Security Level&lt;/code&gt; &lt;code&gt;High&lt;/code&gt;, &lt;code&gt;SSL/TLS&lt;/code&gt; &lt;code&gt;Full&lt;/code&gt;
&lt;img src=&quot;cloudflare-configuration-rules-3.png&quot; alt=&quot;CloudFlare Configuration Rules 3&quot; /&gt;
&lt;img src=&quot;cloudflare-configuration-rules-4.png&quot; alt=&quot;CloudFlare Configuration Rules 4&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Enable WAF&lt;/h2&gt;
&lt;p&gt;Remember my REST API that was accessed with a request count per second?. In the Cloud Run log, I saw that the request came from an IP owned by Amazon. Well, at first I thought that I could just block the IP. However, after checking further, it turns out that the request IP keeps changing. As a DevOps, I know that the IP will keep changing. Instead of thinking about the IP + CIDR, I just block the Amazon ASN 😁.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;Cloudflare&lt;/code&gt;, go to &lt;code&gt;Security&lt;/code&gt; &amp;gt; &lt;code&gt;WAF&lt;/code&gt; then &lt;code&gt;+ Create Rule&lt;/code&gt;
&lt;img src=&quot;cloudflare-waf-rules-1.png&quot; alt=&quot;CloudFlare WAF Rules 1&quot; /&gt;&lt;/li&gt;
&lt;li&gt;For example, I will block Amazon ASN. Fill the configuration as follows:
&lt;ul&gt;
&lt;li&gt;Fill the Rule Name&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Expression&lt;/code&gt; to &lt;code&gt;ASN Num&lt;/code&gt; &lt;code&gt;equals&lt;/code&gt; &lt;code&gt;AS16509&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Action&lt;/code&gt; to &lt;code&gt;Block&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Deploy&lt;/code&gt;
&lt;img src=&quot;cloudflare-waf-rules-2.png&quot; alt=&quot;CloudFlare WAF Rules 2&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;Now, my Cloud Run is protected by Cloudflare. I have added the domain to Cloudflare and enabled WAF to block the Amazon ASN.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cloudflare-waf-rules-blocked.png&quot; alt=&quot;CloudFlare WAF Rules 2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Remember to turn on the Cloudflare orange cloud to enable the Cloudflare proxy. If you don&apos;t turn on the orange cloud, Cloudflare won&apos;t protect your Cloud Run.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Bastille as FreeBSD Jail Management</title><link>https://notes.cnbr.jp/posts/bastille-as-freebsd-jail-management/</link><guid isPermaLink="true">https://notes.cnbr.jp/posts/bastille-as-freebsd-jail-management/</guid><description>How I manage FreeBSD jails using Bastille.</description><pubDate>Wed, 17 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the features I like in FreeBSD is jail. Jail is a feature that allows us to create a virtual environment isolated from the main operating system. I use jail because I want to keep every installed application organized so things don’t get messy.&lt;/p&gt;
&lt;p&gt;I preferred creating jails manually. You can check my configuration at &lt;a href=&quot;https://github.com/icaksh/shinsha-jail&quot;&gt;shinsha-jail&lt;/a&gt;. Some might ask, why do it manually? I use vtnet as the jail network interface. I find it too complicated to configure vtnet manually when the jail is activated. Moreover, I need port forwarding for each jail I set up (perhaps similar to OpenVZ?). So, I prefer the manual approach rather than using existing tools—after all, they are just as complicated.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Until, my shinsha-jail caused Google Cloud Engine (GCE) to be unable to boot when an additional bridge0 interface was added&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was actually quite skeptical about jail management tools because, the concept is the same, not much different from manual configuration. However, since I was tired of seeing my GCE serial console stuck at &lt;code&gt;DHCP renewal in 18000 seconds&lt;/code&gt;, I decided to try using a jail management tool.&lt;/p&gt;
&lt;p&gt;I tried ezjail, but... I didn’t really like it because I still had to configure pf manually. I have read &lt;code&gt;bastille&lt;/code&gt;, reading &lt;code&gt;container&lt;/code&gt; statement like &lt;code&gt;docker&lt;/code&gt; make me sceptical. But, I decided to try it and I was surprised. Bastille is a great tool for managing jails. It is easy to use and has a lot of features.&lt;/p&gt;
&lt;h2&gt;Installing Bastille&lt;/h2&gt;
&lt;p&gt;Bastille have a good documentation for someone who first using it. You can check it at &lt;a href=&quot;https://bastillebsd.org/getting-started/&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why I Use Bastille&lt;/h2&gt;
&lt;p&gt;I use Bastille because it is easy to use and has a lot of features. I can create a jail with a single command. I can also easily manage jails, start, stop, and delete them. Bastille also has a template system that allows me to create jails with pre-configured settings.&lt;/p&gt;
&lt;p&gt;What I love next that Bastille has a ZFS integration. I can create a jail with a ZFS dataset. This is very useful because I can easily manage the storage of the jail. I can also easily take snapshots of the jail and rollback to a previous state if something goes wrong. Bastille also has a function to easily mount a directory from the host to the jail. This is very useful because I can easily share files between the host and the jail.&lt;/p&gt;
&lt;p&gt;I&apos;am to lazy to configure &lt;code&gt;pf&lt;/code&gt; manually. Bastille has a function to automatically configure &lt;code&gt;pf&lt;/code&gt; for the jail. This is very useful because I can easily configure the firewall for the jail. I can also easily configure port forwarding for the jail.&lt;/p&gt;
&lt;p&gt;So, let&apos;s try Bastille. I hope you find it useful. Feel free to reach out to me if you have any questions or feedback.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;// END&lt;/code&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>