Reducing False Positives: Addressing Non-Exploitable CVEs in Hardened Kubernetes Environments with Vulnerability Scanners

cover

cover

Introduction: Addressing False Positives in Kubernetes Vulnerability Scanning

In Kubernetes environments fortified with security measures, traditional vulnerability scanners such as Trivy and Grype frequently generate false positives by flagging theoretically present but practically non-exploitable Common Vulnerabilities and Exposures (CVEs). These scanners operate in isolation, analyzing container images without integrating runtime security contexts, such as Kubernetes hardening configurations. For example, a container with readOnlyRootFilesystem: true and read-only volume mounts renders arbitrary file write CVEs irrelevant, yet scanners lack the contextual awareness to disregard them. This discrepancy stems from the decoupling of static analysis and runtime security, where scanners’ theoretical risk assessments fail to account for actual protections like runAsNonRoot, resource limits, or Pod Security Policies.

This mismatch between static scanning and dynamic runtime conditions creates a critical gap: scanners treat all CVEs as high-priority threats, irrespective of the environment’s security posture. The result is a cascade of unnecessary alerts, leading to alert fatigue, misallocation of resources, and delayed response to genuine threats. Without a standardized mechanism to communicate environment-specific mitigations, security teams are forced into manual triage—an inefficient process that scales poorly with Kubernetes complexity.

vex8s addresses this challenge by integrating a machine learning model that classifies CVEs based on exploitation vectors (e.g., privilege escalation, resource exhaustion) and correlates them with Kubernetes manifests. It generates an OpenVEX document, enabling scanners to suppress non-exploitable CVEs by mapping hardening measures to mitigated exploitation vectors. For instance, a privilege escalation CVE in a container running as a non-root user (runAsNonRoot: true) is flagged as non-exploitable, following the causal chain: hardening measure → neutralized exploitation vector → suppressed alert. This approach transforms vulnerability scanning from a blunt, context-agnostic process into a precise, environment-aware mechanism.

By bridging the gap between static analysis and runtime context, vex8s eliminates noise, delivering actionable insights that enhance security efficiency and reduce operational overhead. As Kubernetes adoption grows, such tools transition from optional to essential. Without them, organizations face escalating false positives, compromising both security efficacy and developer productivity. The solution is clear: vex8s is not just an innovation—it is a critical operational requirement for modern Kubernetes security.

Analyzing the Scenarios: When CVEs Are Not Exploitable

In hardened Kubernetes environments, the disparity between theoretical vulnerabilities and real-world exploitability is pronounced. Traditional vulnerability scanners, such as Trivy and Grype, operate in isolation, analyzing container images without contextual awareness of runtime configurations. This disconnect results in false positives, where CVEs are flagged as critical despite being neutralized by Kubernetes security measures. Below, we examine six scenarios where vex8s identifies and suppresses non-exploitable CVEs, elucidating the causal mechanisms that link hardening measures to alert suppression.

1. readOnlyRootFilesystem Neutralizes Arbitrary File Write CVEs

When the readOnlyRootFilesystem setting is enabled, the container’s root filesystem is mounted as read-only. This configuration directly blocks arbitrary file write operations, rendering CVEs such as CVE-2023-XXXX non-exploitable. vex8s detects this setting in the Kubernetes manifest, correlates it with the CVE’s exploitation vector (file modification), and suppresses the alert. Causal mechanism: readOnlyRootFilesystem enforces read-only access → write operations are blocked → arbitrary file write CVE is neutralized.

2. runAsNonRoot Mitigates Privilege Escalation CVEs

The runAsNonRoot setting mandates that containers execute as non-root users, eliminating the ability to perform privileged operations. CVEs requiring root privileges for exploitation, such as CVE-2022-YYYY, are thus rendered inert. vex8s cross-references the CVE’s exploitation requirements with the manifest, identifies the mitigation, and generates an OpenVEX document to suppress the alert. Causal mechanism: non-root user enforcement → privilege escalation vectors are blocked → CVE is neutralized.

3. Resource Limits Prevent Resource Exhaustion Attacks

Kubernetes resource limits (CPU, memory) constrain container resource consumption, preventing attackers from exploiting resource exhaustion vulnerabilities. CVEs such as CVE-2021-ZZZZ, which rely on excessive resource consumption, are mitigated because the container cannot exceed predefined thresholds. vex8s maps these limits to the CVE’s exploitation vector, suppressing the alert. Causal mechanism: resource limits enforce consumption caps → resource exhaustion attacks are thwarted → CVE is neutralized.

4. Seccomp Profiles Block System Call Abuse

Restrictive Seccomp profiles limit the system calls a container can execute, neutralizing CVEs that depend on specific syscalls. For example, CVE-2020-AAAA is rendered non-exploitable if the profile blocks the required syscall. vex8s analyzes the Seccomp profile, identifies blocked syscalls, and suppresses relevant CVEs. Causal mechanism: syscall restrictions enforced by Seccomp → attack vectors are inaccessible → CVE is neutralized.

5. Network Policies Isolate Containers from Lateral Movement

Kubernetes network policies enforce pod-to-pod communication restrictions, preventing lateral movement within the cluster. CVEs enabling lateral movement, such as CVE-2019-BBBB, are mitigated if network policies isolate the container. vex8s evaluates these policies, maps them to exploitation vectors, and suppresses alerts. Causal mechanism: network isolation enforced by policies → lateral movement is blocked → CVE is neutralized.

6. Immutable Workloads Render Code Injection CVEs Harmless

In immutable deployments, containers are never modified post-deployment, eliminating the possibility of code injection. CVEs such as CVE-2018-CCCC, which exploit code injection, are non-exploitable in this context. vex8s detects immutability in the manifest, suppresses the alert, and documents the mitigation in OpenVEX. Causal mechanism: immutability enforces unalterable code → code injection is blocked → CVE is neutralized.

Practical Insights: Bridging Static Analysis and Runtime Context

The core innovation of vex8s lies in its ability to systematically correlate Kubernetes hardening measures with CVE exploitation vectors. By integrating a machine learning model, vex8s classifies CVEs by attack type (e.g., file write, privilege escalation) and precisely maps these to runtime configurations. This transforms vulnerability scanning from a static, context-agnostic process into a dynamic, environment-aware mechanism. Without such tools, organizations face a security risk cascade: false positives lead to alert fatigue, delaying responses to genuine threats and compromising the overall security posture.

For practitioners, understanding these scenarios enables proactive hardening and informed suppression of non-exploitable CVEs. vex8s not only reduces noise but also redefines vulnerability management in Kubernetes, aligning security practices with real-world risk assessment.

Solutions and Best Practices: Mitigating False Positives in Hardened Kubernetes Environments

False positives in container vulnerability scanning stem from a fundamental disconnect between static analysis and runtime security contexts. In hardened Kubernetes environments, where security measures such as readOnlyRootFilesystem, runAsNonRoot, and resource limits are systematically enforced, traditional scanners like Trivy and Grype often flag CVEs that, while theoretically present, are rendered non-exploitable by these runtime protections. This section deconstructs the problem and presents actionable strategies to minimize false positives, grounded in the mechanisms by which vulnerabilities are neutralized in production deployments.

1. Scanner Configuration: Enhancing Context-Aware Analysis

Conventional vulnerability scanners operate in isolation, analyzing container images without integrating runtime security contexts. To mitigate false positives, scanners must incorporate environment-specific protections:

  • Enable Kubernetes Integration: Where supported, connect the scanner to the Kubernetes API server to ingest runtime configurations (e.g., Pod Security Policies, Security Contexts) during scanning. Mechanism: The scanner cross-references image vulnerabilities with runtime settings, identifying discrepancies between theoretical and actual risk exposure.
  • Adjust Severity Thresholds Dynamically: Configure scanners to disregard low-severity CVEs unless they align with known exploitation vectors in the environment. Example: A “medium” severity CVE requiring root privileges is irrelevant if **runAsNonRoot* is enforced, as the prerequisite for exploitation is absent.*
  • Strategic Exclusion Lists: Maintain curated exclusion lists for CVEs mitigated by hardening measures. Caveat: Manual curation is resource-intensive and lacks scalability without automation.

2. Kubernetes Hardening: Mechanisms of Exploitation Neutralization

Hardening measures directly disrupt exploitation vectors, rendering specific CVEs non-exploitable. The following table maps Kubernetes settings to their neutralization mechanisms:

Hardening Measure Mechanism CVE Neutralization Example
readOnlyRootFilesystem Enforces a read-only root filesystem, preventing write operations. CVEs like CVE-2023-XXXX (arbitrary file write) are neutralized as the filesystem cannot be modified.
runAsNonRoot Mandates non-root user execution, eliminating privileged operations. CVEs like CVE-2022-YYYY (privilege escalation) are rendered harmless as root access is unavailable.
Resource Limits Imposes CPU and memory caps, preventing resource exhaustion attacks. CVEs like CVE-2021-ZZZZ (excessive resource consumption) are mitigated as containers cannot exceed predefined limits.
Seccomp Profiles Restricts executable system calls, blocking syscall-dependent attacks. CVEs like CVE-2020-AAAA (specific syscall exploitation) are non-exploitable as the syscall is blocked.

3. Automating Context-Aware Suppression with vex8s

Manual suppression of false positives is operationally unsustainable. vex8s automates this process through:

  • Exploitation-Based CVE Classification: Its embedded machine learning model categorizes CVEs by attack vectors (e.g., arbitrary file write, privilege escalation). Mechanism: The model analyzes CVE metadata and historical exploit data to infer potential attack paths.
  • Kubernetes Manifest Correlation: vex8s parses Kubernetes manifests to identify hardening measures. Example: If **readOnlyRootFilesystem: true* is detected, all arbitrary file write CVEs are flagged as non-exploitable.*
  • OpenVEX Document Generation: Produces standardized OpenVEX documents consumable by scanners to suppress irrelevant alerts. Outcome: Transforms static scanning into a dynamic, environment-aware process.

4. Edge-Case Analysis: Limitations of Hardening Measures

Hardening measures are not infallible. The following edge cases illustrate residual risk:

  • Partial Hardening: If readOnlyRootFilesystem is enabled but a volume mount remains writable, arbitrary file write CVEs targeting that volume remain exploitable. Mechanism: The writable volume serves as an attack surface, bypassing the read-only restriction.
  • Misconfigured Policies: A Seccomp profile permitting ptrace system calls can expose containers to privilege escalation, even with runAsNonRoot enforced. Mechanism: ptrace enables process tracing, circumventing user-level restrictions.

5. Practical Implementation: Aligning Security with Developer Velocity

Mitigating false positives requires integrating security into development workflows:

  • CI/CD Pipeline Integration: Deploy vex8s or similar tools within CI/CD pipelines to suppress false positives pre-deployment. Impact: Reduces alert fatigue and prevents unwarranted build failures.
  • Developer Education: Ensure teams understand how Kubernetes hardening measures neutralize specific CVEs. Example: Clarify how **runAsNonRoot* renders privilege escalation CVEs inert.*
  • Configuration Drift Monitoring: Use policy engines like Kyverno or OPA Gatekeeper to enforce hardening measures and detect misconfigurations. Mechanism: Policy engines continuously validate Kubernetes settings against predefined rules.

Without tools like vex8s, organizations face escalating operational costs from false positives. By bridging the gap between static analysis and runtime security, these solutions not only reduce noise but also enhance the accuracy of risk assessments. As Kubernetes adoption grows, context-aware vulnerability management is no longer optional—it is imperative for maintaining both security and operational efficiency.

AI Fatigue for Developers: Managing Cognitive Overload from Code Assistants

The constant stream of suggestions from AI code assistants is creating a new kind of mental tax. The problem isn’t prompt engineering or model accuracy. It’s a deeper issue of control and focus, leading to what some engineers are calling AI fatigue.

When your IDE is constantly suggesting entire blocks of code, your job changes from creating to validating. You become a full-time reviewer for a junior developer who never sleeps, never learns your project’s specific context, and never gets tired. This model of passive assistance has a real, non-obvious cost. It trades the focused effort of building for the scattered effort of auditing, which can compromise the system’s integrity and the deep thinking that good engineering requires.

The silent cost of constant suggestions

Code assistants see a local window of your code, but they have no architectural awareness. They can generate a function that correctly implements an algorithm, but they don’t know about the new data access pattern the team agreed to last month or the long-term plan to deprecate a library. The developer is left to catch these subtle, critical deviations.

Shifting the review burden to the engineer

Every AI suggestion is a tiny pull request. You have to stop, read the suggestion, parse its logic, check its correctness, and make sure it aligns with the project’s architecture. This is a real context switch that interrupts your train of thought.

When you write code from scratch, the logic flows from your mental model of the system. When you review an AI suggestion, you first have to reverse-engineer the AI’s logic and then map it back to your own. This validation cycle, repeated dozens or hundreds of times a day, fragments your attention. The cognitive load isn’t reduced, it’s just changed from a focused block of creative work into a scattered series of validation checks.

Why passive assistance erodes critical thinking

Deep work in software engineering requires holding a complex problem in your head. You build a mental model of how components interact, how data flows, and where things might fail. Constant suggestions from an AI assistant actively work against this. The stream of code snippets keeps your attention at the surface level, focused on the next few lines instead of the overall structure.

This continuous interruption prevents the sustained focus you need to see a better abstraction, question a requirement, or spot a potential performance bottleneck. The AI optimizes for local correctness, while an experienced engineer optimizes for the system’s global health. By offloading the “easy” parts, we risk losing the context that helps with the hard parts.

AI fatigue: A new source of engineering debt

The speed gains from AI assistants are immediate and easy to measure. The architectural drift and fuzzy ownership that come with them are not. This asymmetry creates a new, quiet form of engineering debt.

The subtle drift in code ownership

When you accept an AI suggestion, a small part of the code’s “why” is lost. The code is there and it works, but the reasoning behind its structure and the alternatives that were discarded exist only in the model’s latent space. You, the author, don’t have the same depth of ownership because you didn’t go through the process of creating it.

When it comes time to refactor or debug that code six months later, that lack of deep context is a liability. The team has a block of code that is technically sound but feels architecturally foreign. No one feels a strong sense of ownership, which makes it harder to evolve or fix.

When speed obscures architectural intent

An AI assistant will happily generate code that violates your team’s established patterns if it saw a different, more common pattern in its training data. It might use a direct database call where you’ve built a repository layer, or implement custom state logic in a component that should be using a centralized store.

These small deviations seem harmless by themselves. They get the immediate task done faster. Over time, they break down the architectural coherence. The system becomes a patchwork of different patterns, making it harder to understand, maintain, and test. The speed you gained on the first commit is paid back with interest during every future maintenance cycle.

Defining AI boundaries: A control-first approach

The answer isn’t to abandon these tools. We need to shift from passive, continuous assistance to explicit, on-demand use. The developer must be in control, deciding when and how to ask for help.

Explicit ways to use AI

Instead of a constant stream of suggestions, we can use AI for specific, high-value tasks.

  • Scaffolding on demand. Use the AI for boilerplate. Ask it to generate a new gRPC service with stubs, a new component with a test file, or a CI/CD pipeline from a template. This is a one-shot generation task that saves setup time without interfering with the core logic.
  • Generation from your patterns. For well-defined, repetitive tasks, show the AI examples of your own code. Give it a few existing data models and their repository classes, then ask it to generate a new repository for a new model. This constrains the AI to your project’s specific conventions.
  • Help with refactoring. Use AI for mechanical changes on a selected block of code. Tasks like converting a `for` loop to a `map`, extracting a method, or changing a Promise chain to async/await are well-suited for automation. The scope is small and your intent is clear.

Team policies for code assistant output

To manage the code that comes from AI, teams need simple conventions. This is about managing risk and maintaining clarity.

  • Flag AI-generated code in reviews. A simple comment like `// AI-generated, reviewed for correctness and style` or a pull request label tells the reviewer to apply a different kind of scrutiny. The focus should be less on syntax and more on architectural fit.
  • Check for architectural compliance. The main job of a human reviewer for AI-assisted PRs is to be an architectural backstop. Does this code use the right data layer? Does it follow our error handling standards? Does it introduce dependencies we are trying to remove?
  • Document AI use in pull requests. A brief note in the PR description about which parts were AI-generated helps build a history. If you start seeing bugs from AI-generated code, this data becomes useful for refining team policies.

Measuring AI’s impact on architecture and reliability

Productivity claims for AI often focus on lines of code or commit frequency. These metrics miss the point. The real measure of a tool’s effectiveness is its impact on our mental state, the system’s reliability, and its long-term maintainability.

Evaluating the real cognitive load

We need to measure the total effort, not just the time it takes to write the first draft.

  • Compare time spent. For a given feature, track the full cycle time. Does the time saved in generation get eaten up by validation, debugging, and refactoring?
  • Track bugs from AI code. When filing a bug, add a field to identify if the code was known to be AI-generated. Over time, you can see if there is a correlation between AI use and certain types of defects, especially subtle logic or integration bugs.
  • Ask developers how they feel. Do they feel more focused or more fragmented? Do they feel like they are spending more or less time in a state of deep work? Anonymous surveys can provide honest feedback on the perceived mental cost.

Prioritizing deep work over suggestion streams

The goal of any developer tool should be to get out of the way so engineers can solve hard problems. AI assistants, in their current always-on form, often do the opposite. They pull your attention to the surface and encourage a reactive workflow.

By establishing clear boundaries and using AI for specific, targeted tasks, we can reclaim control. The goal is to make AI a tool you pick up for a job and then put down, not a constant presence that reshapes how you think. The most valuable work in engineering still happens during quiet, uninterrupted stretches of deep thought, and our tools should protect that focus.

What makes a good MCP tool definition (and what makes a terrible one)

MCP tools are simple in theory: you define a function, describe what it does, and AI models call it when relevant.

In practice, the quality of your tool definition determines whether your AI is actually useful or constantly confused.

Here are the patterns that separate good from bad.

The naming problem

Bad:

{
  "name": "get_data",
  "description": "Gets data from the system"
}

This is useless. Every tool “gets data.” The AI has no idea when to call this vs anything else.

Good:

{
  "name": "get_overdue_device_patches",
  "description": "Returns a list of devices that have not received a security patch in the specified number of days. Defaults to 30 days if not specified."
}

Now the AI knows exactly when to call this — when someone asks about patch status, overdue updates, or device compliance.

Rule: Name your tool like a function a senior engineer would write. Verb + noun + context.

The parameter problem

Bad:

{
  "parameters": {
    "query": {
      "type": "string",
      "description": "The query"
    }
  }
}

What query? SQL? Natural language? A search string? The AI has to guess.

Good:

{
  "parameters": {
    "days_since_last_patch": {
      "type": "integer",
      "description": "Number of days since last patch. Returns devices that haven't been patched within this window. Default: 30",
      "default": 30
    },
    "os_filter": {
      "type": "string",
      "description": "Optional: filter by OS type ('windows', 'macos', 'linux'). Omit to include all.",
      "required": false
    }
  }
}

Explicit parameters with real descriptions. The AI knows exactly what to pass.

The scope problem

The most common mistake: tools that try to do too much.

Bad: One tool that takes a “query type” parameter and branches internally.

Good: Separate tools for separate concerns. get_active_devices, get_overdue_patches, get_device_by_id.

Small, focused tools are easier for AI models to reason about. They also fail more gracefully.

Real-world example

At Conexor.io, we generate MCP tool definitions automatically from database schemas. The quality of those definitions directly affects query accuracy.

When we added column-level descriptions and specific parameter types, the accuracy of AI-generated queries improved significantly — fewer hallucinated column names, fewer type mismatches.

The lesson: MCP tool quality is a first-class concern, not an afterthought.

Good tools make AI feel like it knows your system. Bad tools make it feel like it’s guessing.

The difference is almost entirely in the definition.

I built a CLI tool to stop copy-pasting my backend boilerplate

Every time I started a new Node.js project I was doing the same thing:

  • create src/db/db.js
  • create src/app.js
  • set up express, cors, dotenv
  • copy asyncHandler, ApiError, ApiResponse utils

So I built templr-js.

npm install -g templr-js

save your perfect setup once

templr save mybackend

use it forever

templr use mybackend

No cloud. No config files. Stored locally on your machine.
node_modules and .git are never copied.

Built it in a day with pure Node.js — fs, path, os. No dependencies.

npm: npmjs.com/package/templr-js
github: https://github.com/Ammarfrfr/templr-js

My Journey From X Explorer to Indie Hacker

Hey there!
Welcome to my personal blog, and glad to know somehow you come here. So, first, let me tell you that, this blog is written by me & not any AI tool. So, Its human to human sharing. We’re in super fast going world, nobody has time for outsider, Because we’re into fast moving AI world. Somehow, its reminding me of “Genesis” from Terminator series, It was my favourite movie series & I watched it on repeated more than 100 times.

I’m into strange situation, I’m 26 year old, and I’m IT company employee doing 9-5 like most of people start their career. I have no magic skill or no extra talent. I have certain characteristics like finding always best thing, in anything, best movies to watch, best books to read, best places to travel, best home things to use. I always look for the best, and so that, by default my research skills is really good. I’m good at pattern recognition, I don’t like to do repetitive boring work, I have diverse area of interest, I want to do everything.

But, now, let’s go back to in 2020, corona hit, come to 2021, chatGPT comes, then GitHub copilot comes, and then, cursor and then perplexity and now claude code , and codex and then Openclaw, meanwhile we all gone through MCP and all those thing.

Like everyone, who are active on X, I seen people building on Build In Public community. I’m still figuring out, because, its hard to make your first dollar with your product, to be honest, freelancing is much easier, you cold dm/email someone, show your work and get work, Its easy. But building your product, teach you more than anything, Now a day building any product is someone anyone can do it, now, product like, lovable, laracopilot, Claude code, Cursor , made things easy, you can ship production ready app in less than 48 hours with payment ready.

Still, why people are failing and why I’d failed or give up on midway before seeing real results, I built almost 4-5 indie products as a Indie-hacker. I love build in public and engaging with community, I real love talking to people from different countries. That’s the journey you’ll enjoy, those secret group you make on journey. Those call you make with people, those personal DM conversation. Those growth hacking tips, those product validation ask.

But, its journey to you enjoy and when you see real results, I can’t explain how you feels actually.

So, now, let’s come to what I’m building & why I’m building, what I shipped till now.

My first product was promptnoon, its vibe coded platform through lovable and cursor. I keep finding top-notch prompts and i keep loosing prompts, that’s why finally, i decided to build a platform where I can save my any prompts, which either I create or find, I don’t need to save in my notes, WhatsApp dm or in notion, or in google doc. & finally I launched my first free platform back in august 2025, with no payment integration, free to use, no plan to monetize, I done marketing while building in public on X & on sharing in subreddit in comment and via post, I crossed almost 100+ users, but there’s no thinking to monetize it. Then, in between, as trend changes, I come to next business idea.

By the time 3-4 month passes, I was working in product related to digital identity verification and then I got idea of building Privacy-First Consent Management for India as there’s gap in Indian market. There’s no provider from India. so, i started building it, launched Accordkit in November. But I know it was long term game, I started doing marketing on linkedin & X, for almost 2 month, started doing cold-dm, but seen no results. So, I left it dormant for now.

Then in November, I got inspired from laramap and launched Indiemap for Indie-hackers & solo-founders. It was one map to gather all indie-hackers to connect with same builder same city from worldwide. Currently, I have 70+ indie-hackers there. I done build in public marketing for almost 2 months, then I took break due to family function and due to wedding season. I integrated payment gateway to monetize it with popular polar.sh for indie builders.

But then, now see any solid way to monetize it, now, its ideal as of now.

Now, finally after wedding season over here in India. I get back to building, I launched Nextjs Development Agency – Aizecs. I started working on SEO & totally relying on in-bound leads. Meanwhile, I launched my personal brand website – avinashvagh where I offer end to end product marketing & GTM strategy & execution in feb, 2026. Working on SEO and cold dm strategy relies on outbound and inbound for productize services.

Now, finally In March, I launched light weight alternative of product hunt which is NoonLaunch. A platform to launch startup product and get do-follow backlink and get traction for early products.

That’s my journey in short, let’s see way forward & how it goes.

If you like this raw writing and raw journey with no sugar coating words. Connect with me on X. Yet, I’m figuring out for product which make me real money apart from my 9-5.

So, let’s go indie, Let’s keep building.

Untill next time,

Avinash

Advanced Terraform Module Usage: Versioning, Gotchas, and Reuse Across Environments

Day 9 of my 30-Day Terraform Challenge focused on moving beyond basic Terraform modules into more practical, real-world infrastructure patterns.

Today’s learning was centered around three key areas:

  • Module gotchas
  • Module versioning
  • Reusing modules safely across multiple environments

This was one of the most useful Terraform days so far because it introduced concepts that are essential when working in real teams and production environments.

Why This Matters

Terraform modules make infrastructure reusable and easier to manage, but they can also introduce subtle bugs and inconsistencies if not designed carefully.

In real-world DevOps and cloud engineering work, infrastructure should be:

  • Reusable
  • Predictable
  • Versioned
  • Safe across environments

That is exactly what today’s work helped me understand.

Module Gotcha #1: File Paths Inside Modules

One of the most common mistakes when working with Terraform modules is referencing files using relative paths without considering where Terraform is being executed from.

This becomes a problem when a module depends on files such as startup scripts, templates, or configuration files. If the file path is not handled correctly, Terraform may fail to locate the file.

The safer and more reliable approach is to always reference files relative to the module itself rather than where Terraform is being run. This makes the module portable and easier to reuse.

This was a very useful reminder that modules should always be written with portability in mind.

Module Gotcha #2: Inline Blocks vs Separate Resources

Another common issue comes from mixing inline resource configuration with separate resource definitions.

A good example is security group rules. Some Terraform resources allow rules to be defined directly inside the resource, while also allowing them to be created separately.

Mixing both approaches in the same module can lead to conflicts, confusing behavior, and harder debugging.

The better practice is to choose one style and remain consistent. Using separate resources tends to be more flexible and makes it easier to extend modules later without editing their internal structure too much.

This is a small design decision, but it has a big impact on maintainability.

Module Gotcha #3: Module Output Dependencies

One subtle but important gotcha is depending on an entire module when only one specific output or resource is needed.

When Terraform treats a whole module as a dependency, it can cause more resources than necessary to be recreated or reevaluated. This makes plans noisier and can lead to avoidable infrastructure changes.

The better design approach is to expose specific outputs and structure modules so dependencies remain as granular as possible.

This is one of those issues that may not seem serious at first, but in larger projects it can create a lot of unnecessary complexity.

Building a Reusable Module

For this challenge, I worked with a reusable Terraform module called webserver-cluster.

The purpose of this module is to provision a complete web server cluster setup in AWS, including the supporting infrastructure needed to serve traffic reliably.

The module provisions resources such as:

  • A VPC lookup
  • Subnets
  • Security groups
  • An Application Load Balancer
  • A Launch Template
  • An Auto Scaling Group
  • EC2 instances configured to run a simple Python-based web server

It was exciting to see how much infrastructure could be abstracted into a reusable module that can be called from multiple environments.

Versioning the Module

One of the most important parts of today’s challenge was learning how to version Terraform modules.

Instead of always referencing a module directly from a local folder or an unpinned GitHub repository, I learned how to use version tags to control exactly which module release an environment should use.

This is a very important practice because infrastructure code changes over time. Without versioning, an environment could suddenly behave differently just because the source module was updated.

To solve this, I created versioned releases for my module:

  • v0.0.1 — the initial reusable module release
  • v0.0.2 — an updated version with a new customizable input variable

This allowed me to safely introduce improvements while keeping full control over which environments used which version.

What Changed Between Versions

The key improvement I introduced in the second version of the module was the ability to customize the text displayed by the web server.

This made the module more flexible and more useful across different environments.

For example, development environments can now display a custom message that clearly identifies them as development deployments, while production can remain more stable and unchanged.

This may seem like a small feature, but it demonstrates the real value of versioning: making controlled improvements without breaking existing environments.

Multi-Environment Reuse

This was the part of the challenge that tied everything together.

I configured different environments to intentionally use different module versions:

  • Development uses the newer module version
  • Production stays pinned to the older stable version

This is a best practice because development is where changes should be tested first. Production should only move to a newer module version after that version has been validated.

This approach helps teams reduce risk, avoid unexpected infrastructure changes, and maintain confidence in production deployments.

It also reflects how real engineering teams manage infrastructure releases in a controlled way.

Why Version Pinning Matters

One of the biggest lessons from today is that not pinning module versions is risky.

If a shared module changes and environments are not pinned to a specific release, then different engineers may end up deploying different infrastructure without realizing it.

That can lead to:

  • Inconsistent deployments
  • Unexpected behavior
  • Hard-to-debug infrastructure issues
  • Production instability

Version pinning helps solve this by making infrastructure behavior consistent and predictable.

It gives teams control over when and how changes are introduced.

Challenges I Faced

One of the biggest practical challenges I encountered was accidentally committing Terraform-generated files into Git.

These included:

  • .terraform directories
  • terraform.tfstate files
  • provider binaries

This made my repository extremely large and caused push failures.

I fixed this by cleaning up the repository and adding a proper .gitignore file so that only the actual source files were tracked.

This turned out to be a valuable lesson on its own.

Infrastructure repositories should be clean, lightweight, and focused only on reusable source code—not generated artifacts.

Key Takeaways

Today’s challenge taught me several practical lessons:

  • Modules should be written to be portable and reusable
  • Small design choices inside modules can cause major issues later
  • Versioning is essential for safe infrastructure reuse
  • Development and production should not always move at the same speed
  • Clean Git practices are just as important in Infrastructure as Code as they are in software development

These are the kinds of details that make the difference between writing Terraform that “works” and writing Terraform that is safe, maintainable, and team-ready.

Final Thoughts

Day 9 was one of the most practical and eye-opening parts of my Terraform challenge so far.

I moved beyond simply creating modules and started thinking more like a real infrastructure engineer:

  • How do I make this reusable?
  • How do I make this safe for teams?
  • How do I control change across environments?

Those are the kinds of questions that matter in real production work.

I’m really enjoying how each day of this challenge builds not just technical skill, but also better engineering habits.

Connect With Me

I’m documenting my journey through the 30-Day Terraform Challenge as I continue learning more about:

  • Terraform
  • AWS
  • Infrastructure as Code
  • DevOps best practices

If you’re also learning cloud or IaC, feel free to connect.

Terraform #IaC #AWS #DevOps #CloudComputing #30DayTerraformChallenge #TerraformChallenge #Inf

Building a “Wevo” – Modeling a Real world promise.

Hi guys :p 👯‍♂

Welcome to Wevo series.

As I introduced in my previous post, I built Wevo.
https://dev.to/h1d3mun3/introducing-wevo-building-a-trust-history-app-with-swift-vapor-340g

In this series, I’ll describe how Wevo is built.

Let’s Dive in.

Motivation

First, let me explain why I built Wevo.

As I posted before, I believe our trust history and related information should belong with us.

Trust is built from many properties.
History, Details, reactions, date… and so on.

So, I believe these information should be in our hands.

But in reality, this isn’t the case.

Our trust history is locked inside platforms.

There are many options to create trust in each other.
(for example, Slack, linkedin, gmail, X(formerly Twitter), facebook..)

If you made a promise on a Slack, and if you want to move promise history to gmail?

that’s not. you can’t move your trust history to other services.

This is why I built Wevo.

I also had a personal problem: I couldn’t remember all the appointments properly.

Make a “Real world” promise model

Organize the flow of the agreement

Next, let’s talk about how I made a promise model.

In the real world, many promises follow this sequence.

  1. One person proposes to another.
  2. The counterparty accepts the proposal.
  3. Both parties fulfill their duties.
  4. The promise is fulfilled.

So, I need to create a model that can at least represent this much.

But, not every promise is fulfilled.
Some promises are not fulfilled due to some reason.

If a promise is not fulfilled, it follows this sequence instead:

  1. One person proposes to another.
  2. The counterparty accepts the proposal.
  3. One of them didn’t fulfill their duty.
  4. The promise is not fulfilled.

In addition, this can also happen.

  1. One person proposes to another.
  2. The counterparty doesn’t accept the proposal.

I also need to create models that can represent these aspects.

Naming

After organizing the promise sequences, I needed to name each state.

I named each action as follows.

Action Description
proposed One person proposes to another
signed The counterparty accepts the proposal
parted One of them rejects the proposal
honored Mark promise as fulfilled

And named each state as follows.

State Description
proposed Created after proposal. Counterparty has not yet signed.
signed Counterparty has signed.
dissolved Counterparty rejected the proposal, or one party withdrew after signing.
completed Both parties marked the promise as honored.

Next Post

In next post, I will explain how to express agreement in a promise and how to verify its status.

Cheers! 🤙
Happy coding~ 💻😙💻

Optimizing encrypted P2P file transfer – from 225 to 441 MB/s

Part of the KEIBIDROP development blog. KEIBIDROP is in active development. Release is coming soon.

KEIBIDROP transfers files between two peers over encrypted gRPC. The full stack:

Disk I/O -> FUSE kernel -> FUSE daemon -> gRPC framing -> ChaCha20-Poly1305 -> TCP -> Peer

We built micro-benchmarks for each layer and measured throughput with 1GB files on an Intel MacBook Pro.

Baseline numbers

Layer Throughput Overhead
Raw disk (SSD) ~5 GB/s —
Raw gRPC (no encryption) 981 MB/s 5x vs disk
Encrypted gRPC (ChaCha20) 437 MB/s 2.2x vs raw gRPC
FUSE end-to-end 225 MB/s 1.9x vs encrypted gRPC

The encryption layer costs 2.2x. FUSE adds another 1.9x.

Six optimizations

1. Cache the AEAD cipher. The original code created a new ChaCha20-Poly1305 cipher for every message. Caching it in the constructor is safe because the nonce is a monotonic counter.

// Before: creating cipher per-message
aead, _ := chacha20poly1305.NewX(s.key)  // expensive!

// After: created once in constructor
type SecureWriter struct {
    aead cipher.AEAD  // created once
}

2. Single combined TCP write. Two syscalls per message (header + payload) became one. At 512KB chunks, this halves the write() call count.

3. In-place decryption. aead.Open(ciphertext[:0], nonce, ciphertext, nil) reuses the ciphertext buffer instead of allocating a new one. ~2000 fewer allocations per 1GB transfer.

4. Async cache writes in FUSE Read. FUSE needs the data returned to the kernel. Writing that data to the local cache file can happen in a background goroutine. A WaitGroup ensures everything completes before file close. Cache write overhead dropped from 16% to 1.8%.

n := copy(buff, data)
if cacheFD != nil {
    f.CacheWg.Add(1)
    go func() {
        defer f.CacheWg.Done()
        cacheFD.WriteAt(cacheData, cacheOffset)
    }()
}
return n

5. Push-based StreamFile RPC. The original streaming used request-response per chunk. A new server-streaming RPC pushes all chunks without waiting. On-demand reads (random access) still use bidirectional streaming.

6. Silencing hot-path logs. ~120 slog.Info/Debug/Warn calls in FUSE handlers. Even structured logging allocates. Commenting out non-error logs on Read/Write/Getattr gave 2-6% throughput improvement.

After optimizations

Layer Before After Gain
Encrypted gRPC 437 MB/s 441 MB/s +0.9%
FUSE end-to-end 225 MB/s 231 MB/s +2.7%

The encryption tax: 1.49x

We built a controlled benchmark comparing identical gRPC transfers over plain TCP vs ChaCha20-Poly1305:

Transport MB/s Duration (100MB)
Plain TCP + gRPC 657 152ms
Encrypted (ChaCha20-Poly1305) 441 227ms

33% of transfer time goes to encryption. Our earlier 2.2x estimate was comparing across different test runs with different system load. The controlled A/B benchmark showed the real cost is much lower.

The irreducible 48%

The gap between encrypted gRPC (441 MB/s) and FUSE end-to-end (231 MB/s) is 48%. Every FUSE Read requires two kernel-to-userspace context switches. We confirmed this by measuring raw FUSE overhead (local file read through FUSE vs direct): ~48%, matching exactly.

Plain gRPC:     657 MB/s  (ceiling)
  -33% encryption
Encrypted gRPC: 441 MB/s
  -48% FUSE kernel overhead
FUSE E2E:       231 MB/s

The FUSE overhead is nearly twice the encryption cost. If we wanted to improve throughput, reducing FUSE transitions would have more impact than switching ciphers. But FUSE is the cost of running a userspace filesystem, and for secure P2P file sharing the tradeoff is worth it.

What we learned

  1. Without isolating each layer, we would have optimized the wrong thing. The biggest win (async cache writes, 16% to 1.8%) was only visible when measured independently.
  2. Caching the AEAD cipher and in-place decryption are tiny code changes with measurable impact at 2000+ messages per second.
  3. Structured logging is not free on hot paths. Even checking the log level has overhead.
  4. FUSE has an inherent 48% overhead from context switches. No amount of userspace optimization can fix this.
  5. Server-streaming eliminates round-trip latency for sequential prefetch. Request-response is still needed for random access.
  6. Our initial 2.2x encryption estimate was wrong. A controlled A/B benchmark (same test, same system) showed 1.49x. Always compare apples to apples.

More from the KEIBIDROP blog: full series | product page | FAQ

From GitFlow to Trunk-Based Development: What Modern Teams Actually Use

When I started working with Git, most teams were using GitFlow but today, many modern teams (especially cloud and microservices) have moved to something much simpler.

In this article, I’ll explain 3 common approaches:

  1. GitFlow (old but still used)
  2. GitHub Flow (simpler)
  3. Trunk-Based Development (modern best practice)

GitFlow (The Old Standard)
The main idea is that you have multiple long-lived branches:

main → production
develop → integration
feature/* → new work
release/* → preparing release
hotfix/* → urgent fixes

But there are some cons like too many branches, slow delivery, and Frequent merge conflicts!
So, today, GitFlow is often considered heavy and outdated for modern apps.

GitHub Flow (The Simple Version)
GitHub Flow simplified everything, and the main idea is:

  • One main branch (main)
  • Short-lived feature branches
  • Pull Requests (PRs)
  • Deploy after merge

This is what many teams use today without even realizing it.

Trunk-Based Development (Modern Best Practice)
This is the approach most modern teams aim for, and the main idea is:

  • One main branch (main)
  • Very short-lived branches (1–2 days)
  • Frequent merges
  • *main * is always deployable

It is very fast delivery but needs automated tests and feature flags for incomplete work.

So the flow has been shifted from:

 **dev branch -> test branch -> prod branch**

to:

**main branch -> dev -> test -> prod (via pipeline)**

How modern teams use it with DevOps
Then Azure DevOps pipelines:

  1. Build the app once
  2. Run all tests
  3. Deploy to Dev automatically
  4. Promote the same build to Test
  5. Promote the same build to Production

Over time, Git workflows have moved from complex and controlled to simple and fast.

GitFlow gave teams structure, but introduced too much overhead
GitHub Flow simplified things and improved delivery speed
Trunk-Based Development takes it further by focusing on continuous integration and fast feedback

Today, the real shift is not just about branches, it’s about how we deliver software.

JavaScript Promises: Transform Your Async Code from Messy to Clean

It’s late at night.

You open Instagram just for a quick scroll.
The app loads… but something feels off.

The profile takes time.
The feed shows up half-loaded.
Likes appear late.
Comments suddenly pop in after a delay.

For a moment, it feels broken.

Now imagine you’re the developer behind that experience.

Somewhere in your code:

  • Data is coming late
  • Steps are not properly ordered
  • One thing depends on another… but the flow isn’t clear

This is exactly where most developers struggle — not with writing code, but with controlling when things happen.

Let’s make this simple and a little more fun.

Step 1: The “Do Everything in Order” Phase (Synchronous JavaScript)

In the beginning, everything feels easy.

Your code runs top to bottom. No surprises.

console.log("1. User");
console.log("2. Feed");
console.log("3. Likes");

Output:

1. User
2. Feed
3. Likes

Nice and predictable.

But imagine this:

What if loading the user takes 3 seconds?

Everything below just waits.

Your app is now just… staring at the user.

Step 2: “Let’s Not Make Users Wait” (Asynchronous JavaScript)

So you try something smarter.

console.log("Start");

setTimeout(() => {
  console.log("User loaded");
}, 2000);

console.log("Keep using app...");

Output:

Start
Keep using app...
User loaded

Now your app feels alive.

Things are loading in the background.

But there’s a small problem:

Things don’t happen in order anymore.

Step 3: “Okay, I Want Control” (Callbacks)

You decide:

“I want things in order… but still async.”

function loadUser(next) {
  setTimeout(() => {
    console.log("User");
    next();
  }, 1000);
}

function loadFeed(next) {
  setTimeout(() => {
    console.log("Feed");
    next();
  }, 1000);
}

loadUser(() => {
  loadFeed(() => {
    console.log("Done");
  });
});

Output:

User
Feed
Done

Works well.

But if you keep adding steps…

Step 4: The “Why Is This So Deep?” Moment (Callback Hell)

loadUser(() => {
  loadFeed(() => {
    setTimeout(() => {
      console.log("Likes");
      setTimeout(() => {
        console.log("Comments");
        console.log("All done");
      }, 1000);
    }, 1000);
  });
});

It works.

But reading it feels like going down a staircase.

Not fun. Easy to mess up.

Step 5: A Cleaner Way (Promises)

Now let’s clean things up.

function loadUser() {
  return Promise.resolve("User");
}

function loadFeed() {
  return Promise.resolve("Feed");
}

Now write flow like this:

loadUser()
  .then((res) => {
    console.log(res);
    return loadFeed();
  })
  .then((res) => {
    console.log(res);
    console.log("Done");
  });

Output:

User
Feed
Done

Much better.

Flat. Easy to follow.

Going Deeper: What a Promise Really Is

A Promise is not just a cleaner syntax.

It’s an object that represents a value that will be available later.

It has three states:

  • Pending → still waiting
  • Fulfilled → success (you get data)
  • Rejected → failed (you get error)

Here’s a simple example:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data loaded");
    // reject("Something went wrong");
  }, 1000);
});

promise
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

Why this matters:

  • resolve() moves the promise to success
  • reject() moves it to failure
  • .then() handles success
  • .catch() handles errors

Small but Powerful Features of Promises

1. Chaining keeps things clean

Each .then() passes data to the next:

Promise.resolve(1)
  .then((num) => num + 1)
  .then((num) => num + 1)
  .then((num) => console.log(num)); // 3

No nesting. Just flow.

2. Handling multiple async tasks

Sometimes tasks don’t depend on each other.

You can run them together:

Promise.all([
  Promise.resolve("User"),
  Promise.resolve("Feed"),
  Promise.resolve("Likes"),
]).then((res) => console.log(res));

Output:

["User", "Feed", "Likes"]

Everything runs in parallel.

Faster and efficient.

3. First result wins

Promise.race([
  new Promise((res) => setTimeout(() => res("Fast"), 1000)),
  new Promise((res) => setTimeout(() => res("Slow"), 3000)),
]).then(console.log);

Output:

Fast

Useful for timeouts or fastest responses.

Step 6: The Easiest Way (Async/Await)

Now the cleanest version:

async function run() {
  const user = await loadUser();
  console.log(user);

  const feed = await loadFeed();
  console.log(feed);

  console.log("Done");
}

run();

Same result.

But this feels natural.

Like reading normal code.

Where Promises Are Really Used (And Why They Matter)

This is not just theory.

Promises are used everywhere in real applications.

Think about apps like Instagram, YouTube, or Netflix.

When you open them:

  • Your profile is fetched
  • Your feed is loaded
  • Images and videos load separately
  • Likes and comments arrive later

All of these happen asynchronously.

Some steps depend on others:

  • You need user data before showing a personalized feed
  • You need posts before loading comments
  • You need authentication before API calls

Promises help manage all this without chaos.

They let your app stay fast, responsive, and organized at the same time.

The Real Upgrade: From “It Works” to “It Feels Smooth”

Here’s the shift that matters.

At first, you write code just to make things work.

Then things get a little complex.

Suddenly:

  • Order breaks
  • Code gets nested
  • Bugs become harder to trace

That’s when you realize:

This isn’t just about code.
It’s about flow.

Every step in your app is a small event.

If those events are not clearly controlled:

  • Data shows up late
  • UI feels inconsistent
  • Users feel something is off

But when you structure it right:

  • Things load in the right order
  • Background work doesn’t block the app
  • Code stays readable even as it grows

That’s the real role of Promises and async/await.

They don’t just fix syntax.

They fix how your app behaves.

And when the behavior is right, the user never notices the complexity behind it.

It just feels smooth.