<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Terminals &amp; Coffee on Medium]]></title>
        <description><![CDATA[Stories by Terminals &amp; Coffee on Medium]]></description>
        <link>https://medium.com/@terminalsandcoffee?source=rss-ca586dfe08ca------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*cK0lQ1Pm8utcnOFKK7_t6A.png</url>
            <title>Stories by Terminals &amp;amp; Coffee on Medium</title>
            <link>https://medium.com/@terminalsandcoffee?source=rss-ca586dfe08ca------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 19 May 2026 18:21:16 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@terminalsandcoffee/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[I Audited a Popular Open-Source AI Assistant.]]></title>
            <link>https://medium.com/@terminalsandcoffee/i-audited-a-popular-open-source-ai-assistant-cd20a0649b22?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/cd20a0649b22</guid>
            <category><![CDATA[vulnerability]]></category>
            <category><![CDATA[openclaw]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[pentesting]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Sun, 22 Feb 2026 04:14:39 GMT</pubDate>
            <atom:updated>2026-02-22T04:14:39.436Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/824/1*fkyUwGSRHc5uqU7CHPqSdg.png" /><figcaption>Might have to frame this TBH</figcaption></figure><h3>(Potentially) 3 CVEs Later, Here’s What I Learned</h3><p>Last weekend I set up <a href="https://github.com/openclaw">OpenClaw</a> — an open-source personal AI assistant that runs locally in Docker. It’s a great project: you get a capable AI coding assistant without sending your data to someone else’s cloud. Privacy by default. Self-hosted. What’s not to love?</p><p>So I eventually did what any paranoid SOC analyst would do on their day off. I<strong> pointed the assistant at its own container</strong> and said: <strong><em>audit yourself</em>.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PKuMnj-KX3Fb-yr9P7J3Rw.png" /></figure><p>What it found was… educational. What happened <em>after</em> — the static analysis, responsible disclosure, <strong>accepted GitHub Security Advisories</strong>, and an iOS app audit — turned into a proper security research project.</p><p>This post covers the full journey: from a casual weekend container audit to (potential) CVEs on a major open-source project.</p><p><strong>TL;DR:</strong> A security engineer extraordinaire 🧑‍🎨 — audited OpenClaw, a popular self-hosted open-source AI assistant, and uncovered serious vulnerabilities. The key findings included a world-writable state directory (exposing config and session data), a gateway silently binding to all network interfaces despite the config saying otherwise, containers running as root, Chrome’s sandbox being disabled, and VNC running with no password. Three GitHub Security Advisories were accepted (with CVEs pending). The broader takeaway: running an AI tool locally in Docker doesn’t make it secure by default — you still need to lock down file permissions, network bindings, and container privileges yourself.</p><h3>What Is OpenClaw and Why Docker?</h3><p><a href="https://github.com/openclaw/openclaw">OpenClaw </a>is an open-source AI assistant that runs as a local service. You interact with it through a gateway API, and it can execute commands, browse the web, and manage agent sessions on your behalf. Think of it as a self-hosted alternative to cloud-based AI coding tools.</p><p>Most people run it in Docker because containers are <em>supposed to provide isolation</em>. Your AI assistant runs in its own little sandbox, separate from your host system. That’s the theory, anyway.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*27O5VlYxKLDdvZKWzb9OhA.png" /></figure><p>If you’re new to Docker: a container is like a lightweight virtual machine. It has its own filesystem, its own network, and its own processes. A docker-compose.yml file describes how to build and run the container — what ports to expose, what directories to share, what environment variables to set.</p><p>The key word in all of this is <em>supposed to</em>. Containers provide isolation, but only if you configure them correctly. And that’s where things got interesting.</p><h3>The Audit: 8 Findings, 2 Critical</h3><p>I walked through the running container systematically — checked file permissions, network bindings, configuration files, and compared what the config <em>said</em> versus what the runtime <em>actually did</em>. Here’s what came back.</p><h3>CRITICAL — F1: The State Directory Is World-Writable</h3><pre>$ ls -la /home/node/<br> drwxrwxrwx 5 root root 4096 Feb 15 22:31 .openclaw</pre><p>That <strong>rwxrwxrwx</strong> (mode 777) means every single process in the container can read, write, and delete anything inside .openclaw. This directory contains:</p><p>· <strong>openclaw.json</strong> — the gateway configuration file, including the authentication token</p><p>· <strong>Agent session data</strong> — your conversation history and context</p><p>· <strong>Device identity files</strong> — unique identifiers for your instance</p><p>· <strong>Logs</strong> — records of everything the assistant has done</p><p>Now, credit where it’s due: the config file itself (openclaw.json) had permissions set to 600 (owner read/write only), which is correct. But it doesn’t matter if the parent directory is 777. Any process can simply delete the protected file and replace it with their own version. Directory permissions override file permissions in practice.</p><p><strong>Why does this happen?</strong> This is a Docker-on-Windows gotcha. When you mount a volume from a Windows host into a Linux container, Docker (via WSL2) maps the files as root:root with 777 permissions. Windows NTFS doesn’t have Unix permission bits, so Docker just gives everything full access. Most people running Docker Desktop on Windows have no idea this is happening.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CcgNBgQSFRO5aKILlDFFxQ.png" /></figure><p><strong>The fix:</strong></p><p>Inside the container, lock down the state directory</p><pre>chmod 700 /home/node/.openclaw<br>chown -R node:node /home/node/.openclaw</pre><p>Or better yet, add this to your entrypoint script so it runs every time the container starts.</p><p><strong>The lesson:</strong> If you run Docker on Windows or macOS, <strong>always</strong> check the actual permissions of your mounted volumes inside the container. Don’t assume they match what you’d expect on Linux.</p><h3>CRITICAL — F2: Chrome — no-sandbox Disabled OS-Level Browser Sandbox</h3><p>The sandbox browser container launched Chromium with — no-sandbox, which strips away seccomp-bpf filters, namespace isolation, and chroot. A renderer exploit goes straight to code execution without needing a sandbox escape. This is the kind of flag that gets added during development (“it won’t work in Docker without it”) and never gets removed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*nSFyciZd7acO6Rofgk2Z6w.png" /></figure><p>Advisory: <a href="https://github.com/openclaw/openclaw/security/advisories/GHSA-43x4-g22p-3hrq">GHSA-43x4-g22p-3hrq</a></p><h3>CRITICAL — F3: Sandbox Browser noVNC Observer Lacked VNC Authentication</h3><p>The sandbox browser ran x11vnc with -nopw and websockify proxied it to all interfaces. Anyone with network access to the container got full interactive desktop control — no authentication required. This is VNC with no password, exposed to the network, running inside a container that’s supposed to isolate your AI agent’s browsing activity.</p><p>Advisory: <a href="https://github.com/openclaw/openclaw/security/advisories/GHSA-25gx-x37c-7pph">GHSA-25gx-x37c-7pph</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3POWRULsgNMUR9BPrlb-Ow.png" /></figure><h3>LOW: Elevated Privileges and Broad Access</h3><p>A few lower-severity findings rounded out the audit: Intiontially leaving out.</p><h3>The Meta Lessons</h3><p>Three things stood out from this audit that apply far beyond OpenClaw:</p><h3>1. Config drift is real and dangerous</h3><p>Finding F2 is the textbook case. The config file said one thing, the runtime did another. If you only audit config files, you’ll miss this every time. <strong>Always verify at runtime.</strong> Check what ports are actually open. Check what processes are actually running. Check what permissions are actually set.</p><h3>2. “Running in Docker” is not a security strategy</h3><p>Docker provides process isolation and filesystem separation. It does not provide secure defaults. You still need to: — Lock down file permissions — Bind services to the right interfaces — Protect secrets — Minimize the tools and capabilities available inside the container</p><p>A container with 777 permissions and services bound to 0.0.0.0 is just a regular insecure server with extra steps.</p><h3>3. Windows + Docker + Volume Mounts = Permission Chaos</h3><p>If you run Docker Desktop on Windows (which a lot of developers and hobbyists do), every volume mount comes in as root:root with 777. This is a platform-level behavior that most people never check. If you’re running anything security-sensitive in Docker on Windows, you need to explicitly fix permissions in your <strong>entrypoint</strong>.</p><h3>Hardening Checklist for Any Self-Hosted AI Tool</h3><p>Whether you’re running OpenClaw, Ollama, LocalAI, or any other self-hosted AI tool in Docker, here’s a practical checklist:</p><p>☐ <strong>Bind services to 127.0.0.1, not 0.0.0.0</strong> — Both inside the container and in your docker-compose.yml port mappings.</p><p>☐ <strong>Check actual file permissions inside the container</strong> — Run ls -la on config dirs, state dirs, and any mounted volumes.</p><p>☐ <strong>Protect secrets</strong> — .env files should be 600. Better yet, use Docker secrets or a mounted file with restrictive permissions.</p><p>☐ <strong>Verify config vs. runtime</strong> — Use ss -tlnp or netstat inside the container to confirm what’s actually listening.</p><p>☐ <strong>Minimize capabilities</strong> — Disable tools, browser access, and elevated commands you don’t need.</p><p>☐ <strong>Set trustedProxies</strong> if running behind a reverse proxy.</p><p>☐ <strong>Test your deny rules</strong> — Actually try to run blocked commands and verify they fail.</p><p>☐ <strong>Run containers as non-root</strong> — Use USER node (or equivalent) in your Dockerfile.</p><p>☐ <strong>Keep images updated</strong> — docker compose pull &amp;&amp; docker compose up -d on a regular cadence.</p><p>☐ <strong>Review logs</strong> — Check what your AI assistant is actually doing. Trust, but verify.</p><h3>Part 2: Going Deeper — Static Analysis and Responsible Disclosure</h3><p>The runtime audit above was the starting point. But poking around inside a running container only tells you half the story. The <em>build</em> configuration — Dockerfiles, entrypoint scripts, compose files — is where the systemic issues live.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-1wGstETgJ1g4AY63Lls5g.png" /></figure><p>So I pulled the source and did a proper static review.</p><h3>The Static Analysis: Additional Findings</h3><p>Reviewing every Dockerfile, entrypoint script, and the docker-compose.yml against the CIS Docker Benchmark v1.6 turned up 11 additional findings — 3 Critical, 4 High, and 4 Medium. The highlights:</p><p><strong>Chrome’s OS sandbox was disabled.</strong> The browser sandbox container launched Chromium with — no-sandbox, which strips away seccomp-bpf filters, namespace isolation, and chroot. A renderer exploit goes straight to code execution without needing a sandbox escape. This is the kind of flag that gets added during development (“it won’t work in Docker without it”) and never gets removed.</p><p><strong>VNC running with no password.</strong> The sandbox browser ran x11vnc with -nopw and websockify proxied it to all interfaces. Anyone with network access to the container got full interactive desktop control — no authentication required.</p><p><strong>Containers running as root.</strong> Multiple E2E and test Dockerfiles had no USER directive, meaning everything ran as uid 0. And one test container had NOPASSWD:ALL sudo — functionally identical to running as root but with extra steps.</p><h3>Filing the Disclosures</h3><p>I formatted the findings per OpenClaw’s <a href="https://github.com/openclaw/openclaw/security">SECURITY.md</a> requirements and submitted each as a private GitHub Security Advisory. Their required format is straightforward — 8 fields: Title, Severity, Impact, Affected Component, Technical Reproduction, Demonstrated Impact, Environment, Remediation Advice.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*-1wGstETgJ1g4AY63Lls5g.png" /></figure><p><strong>Tip for anyone doing this for the first time:</strong> Read the project’s security policy <em>before</em> writing your report. Every project has different requirements and scope definitions. If you submit a finding that’s explicitly out of scope, you burn credibility on the ones that matter.</p><h3>Part 3: The iOS App — A Different Attack Surface Entirely</h3><ul><li>To be discontinued*</li></ul><h3>What I Actually Learned</h3><h3>1. Read the security policy first</h3><p>The CDP rejection could have been avoided. Five minutes reading SECURITY.md would have told me their threat model explicitly excludes container-neighbor attackers. Know what’s in scope before you spend time writing the report.</p><h3>2. Scope rejections aren’t failures</h3><p>The CDP report was rejected, but Peter shipped a VNC hardening commit the same week. Out-of-scope doesn’t mean the maintainers didn’t hear you — it means it doesn’t qualify as a formal advisory under their policy. The codebase still got more secure.</p><h3>3. The strongest findings cross trust boundaries the project does claim to defend</h3><p>The root-containers finding was accepted immediately because it’s a clear supply chain issue. The — no-sandbox finding was accepted because the browser sandbox is a security boundary they explicitly maintain. The deep link injection is strong because URL scheme handlers are a documented iOS trust boundary.</p><h3>4. Responsible disclosure is a skill</h3><p>Formatting matters. Reproduction steps matter. Understanding the maintainer’s perspective matters. A well-structured report that respects the project’s threat model gets accepted. A technically correct but out-of-scope report gets closed.</p><h3>Closing Thoughts</h3><p>What started as a weekend container audit turned into 3 accepted GitHub Security Advisories, CVEs pending, and multiple findings within an iOS disclosure which is currently in progress. The findings ranged from container privilege escalation to a cross-app deep link injection that lets any app on your phone silently instruct your AI agent to send messages to strangers.</p><p>The irony of auditing an AI assistant’s security is that the assistant is only as safe as the infrastructure it runs on and the trust model it implements. OpenClaw’s application-layer security is actually solid — timing-safe comparisons, SSRF protection with DNS rebinding guards, rate limiting. The gaps were in container hardening and the mobile app’s consent model.</p><p>Self-hosting is still the right call for privacy. But “running locally” doesn’t automatically mean “secure.” You’re the security team now. Act like it.</p><p>If you’re running AI tools locally — and more people are every week — take 30 minutes to run through the checklists above. Most of the Docker fixes are one-liners. The mobile ones require architectural decisions. But the hardest part, as always, is knowing to look.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rm-KJhPeo04d6AAmCMx94g.png" /></figure><p>Stay safe out there.</p><p><em>– Rafael Martinez</em></p><p><strong>Advisories referenced in this post:</strong> —</p><p><a href="https://github.com/openclaw/openclaw/security/advisories/GHSA-w7j5-j98m-w679">GHSA-w7j5-j98m-w679</a> — E2E Dockerfiles run as root</p><p>— <a href="https://github.com/openclaw/openclaw/security/advisories/GHSA-43x4-g22p-3hrq">GHSA-43x4-g22p-3hrq</a> — Chrome — no-sandbox in sandbox browser</p><p>— <a href="https://github.com/openclaw/openclaw/security/advisories/GHSA-25gx-x37c-7pph">GHSA-25gx-x37c-7pph</a> — VNC/noVNC unauthenticated access</p><p>Rafael Martinez is a Cloud Security Engineer. Building cybersecurity tools, AI-powered products, and digital resources at <a href="https://securecloudacademy.gumroad.com/">https://securecloudacademy.gumroad.com/</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cd20a0649b22" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Found a Container Escape -]]></title>
            <link>https://medium.com/@terminalsandcoffee/i-found-a-container-escape-d5c6a167b76b?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/d5c6a167b76b</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[ai-security]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Fri, 20 Feb 2026 04:41:04 GMT</pubDate>
            <atom:updated>2026-02-20T04:41:04.330Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>My First Accepted AI Security Vulnerability Report</strong></h3><p>I almost gave an AI assistant read-write access to my home directory.</p><p>Here’s how a quick code review before installing saved me — and what I found.</p><p><strong>Looking for a Personal AI Assistant</strong></p><p>I’ve been looking at projects that wire up Claude to messaging apps — the idea of having an AI assistant I can text from my phone is compelling. OpenClaw is the big one, but at 52+ modules and dozens of dependencies, it’s a lot of code to trust with access to my machine.</p><p>Then I found <a href="https://github.com/qwibitai/nanoclaw">NanoClaw</a>. Same core idea, fraction of the codebase. One process, a handful of files, agents run in isolated Linux containers. The README pitch is basically: “small enough to understand, secure by isolation.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/814/1*ryBLBbGEokf1EVjImsxSKA.png" /></figure><p>That pitch spoke to me. But “secure by isolation” is a strong claim. And this tool wants access to my filesystem, my API keys, and the ability to run arbitrary commands inside containers that mount my directories.</p><p>So before running `claude` and letting `/setup` do its thing, I did what I always (sometimes) do: I read the code.</p><p><strong>Why Review Before You Install?</strong></p><p>Because working in security will make you paranoid and it’s basic security hygiene, especially for tools that:</p><ul><li>Run with elevated privileges or broad filesystem access</li><li>Execute AI-generated commands autonomously</li><li>Mount your directories into containers</li><li>Hold your API keys and authentication tokens</li></ul><p>NanoClaw is honest about what it does — agents run with `bypassPermissions` inside containers, and your Anthropic API key gets passed in.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/1*cFyVG2nPjt4Biw9ukAU70g.png" /></figure><p>The security model relies on container isolation to keep things safe. If that boundary holds, great. If it doesn’t, the agent has your keys and your files.</p><p>I wanted to know: does that boundary actually hold?</p><p><strong>The Review Approach</strong></p><p>NanoClaw is about 3,500 lines of TypeScript across ~20 files. That’s genuinely small — you can read the whole thing in an afternoon. I focused on the areas that matter for a tool like this:</p><p>1. <strong>Trust boundaries</strong> — Where does untrusted input (WhatsApp messages) meet privileged operations (container mounts, file I/O, shell commands)?</p><p>2. <strong>Container isolation</strong> — What gets mounted? Who controls what gets mounted? Can those controls be influenced by the agent?</p><p>3. <strong>Credential handling</strong> — How are API keys passed to containers? Can the agent access them?</p><p>4. <strong>IPC authorization</strong> — The host and containers communicate via JSON files. What stops a container from doing something it shouldn’t?</p><p>I started by reading `docs/SECURITY.md`, which lays out the trust model cleanly:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/814/1*ws-kHyIeK-WwnA0tZZ1QZw.png" /></figure><p>Good. The developers are thinking about threat modeling. Now let’s see if the implementation matches.</p><p><strong>What I Found: Path Traversal in Group Registration</strong></p><p>NanoClaw lets the main group’s agent register new WhatsApp groups via a `register_group` MCP tool. You give it a group JID, a display name, a trigger word, and a *<strong>folder name*</strong> for storing that group’s data.</p><p>Here’s the MCP tool definition:</p><pre>// container/agent-runner/src/ipc-mcp-stdio.ts folder: <br>z.string().describe(&#39;Folder name for group files (lowercase, hyphens, e.g., &quot;family-chat&quot;)&#39;)</pre><p>That `.describe()` says “lowercase, hyphens” — but that’s documentation, not enforcement. The actual validation is `z.string()`: anything goes.</p><p>That folder value then flows through nine code locations across five files.</p><p><strong>At no point does anyone check for `../` or any other path traversal character</strong>.</p><p>It ends up in `path.join()` calls that construct the host paths for container volume mounts:</p><pre>// src/container-runner.ts<br><br>hostPath: path.join(GROUPS_DIR, group.folder) <br><br>// mounted as /workspace/group (read-write)<br><br>Node.js `path.join()` resolves `..` components:<br>path.join(&#39;/Users/me/nanoclaw/groups&#39;, &#39;../../.ssh&#39;)<br><br>// → /Users/me/.ssh</pre><p>So if the folder is `../../.ssh`, the container gets my SSH directory mounted read-write.</p><p><strong>The Irony: They Already Built Protection for This</strong></p><p>Here’s what makes this interesting. NanoClaw has a dedicated `mount-security.ts` module — 419 lines of careful security logic. It maintains a blocked pattern list:</p><pre>const DEFAULT_BLOCKED_PATTERNS = [<br>&#39;.ssh&#39;, &#39;.gnupg&#39;, &#39;.aws&#39;, &#39;.azure&#39;, &#39;.gcloud&#39;, &#39;.kube&#39;, &#39;.docker&#39;,<br>&#39;credentials&#39;, &#39;.env&#39;, &#39;.netrc&#39;, &#39;.npmrc&#39;, &#39;id_rsa&#39;, &#39;id_ed25519&#39;,<br>&#39;private_key&#39;, &#39;.secret&#39;,<br>];</pre><p>It resolves symlinks before validation. It checks paths against an allow list stored <em>*outside*</em> the project root so containers can’t tamper with it. It supports read-only enforcement for non-main groups. This module does everything right.</p><p>The problem?</p><p>It only applies to “additional mounts” — optional extra directories you configure per-group. The <strong>core mounts</strong> (group folder, sessions directory, IPC directory) are constructed directly from `group.folder` <strong>and bypass the entire mount security system.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/1*pxV8RLaaKZKbYeNv0uW1Gg.png" /></figure><p>The developers clearly understand the threat. They built a comprehensive defense against it. They just missed applying it to the most dangerous input.</p><p><strong>How It Could Be Exploited</strong></p><p>The main group is “Trusted” in the security model — but it processes untrusted input. Every WhatsApp message is potential prompt injection, and the agent runs with full autonomy (`bypassPermissions`). The attack chain:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/825/1*252ZJzUXEPRmFjjFjhL9qQ.png" /></figure><p>1. A WhatsApp message containing prompt injection reaches the main group’s agent</p><p>2. The agent is tricked into calling `register_group` with `folder: “../../.ssh”`</p><p>3. The host process stores this in the database with no validation</p><p>4. When the group’s agent is invoked, `~/.ssh` is mounted read-write into the container</p><p>5. The container agent reads `id_rsa`, `id_ed25519`, or writes to `authorized_keys`</p><p>The agent can even trigger step 4 itself by scheduling a task for the malicious group — making this a single-message, self-contained exploit chain.</p><p><strong>What I Did About It</strong></p><p>I wrote up the finding and checked the repo’s security page. No private vulnerability reporting is enabled, but their `CONTRIBUTING.md` explicitly accepts security fixes:</p><p><a href="https://github.com/qwibitai/nanoclaw/pull/274">security: fix path traversal in register_group folder parameter by TerminalsandCoffee · Pull Request #274 · qwibitai/nanoclaw</a></p><p>&gt; <strong>Accepted:</strong> Bug fixes, security fixes, simplifications, reducing code.</p><p>Shoutout to the developers for jumping right on it and making changes.</p><p>I opened an issue describing the vulnerability with enough detail for the maintainer to understand and fix it, while keeping the full exploit chain out of the public writeup until a fix is in place.</p><p>The fix is a one-liner:</p><pre>// Validate folder: alphanumeric, hyphens, and underscores only<br>if (!/^[a-z0–9–9][a-z0–9_-]*$/i.test(data.folder)) {<br>logger.warn({ folder: data.folder }, &#39;Invalid folder name rejected&#39;);<br>break;<br>}<br><br>// Shoutout AI bc wtf is regex lol</pre><p>For defense in depth, `buildVolumeMounts()` should also verify that resolved paths stay within expected parent directories — the same kind of check that `mount-security.ts` already does for additional mounts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/807/1*pj23KSdKcZXjPVrNnIFYBQ.png" /></figure><p><strong>The Bigger Picture</strong></p><p><strong>NanoClaw is actually a well-built project.</strong> The security model is thoughtful, the code is clean, and the developers clearly care about doing this right. This isn’t a case of “the code is terrible” — it’s a case of a specific input flowing through a path that nobody thought to validate.</p><p>That’s how most real vulnerabilities work. Not dramatic oversights, just a gap between what the developer intended and what the code actually enforces.</p><p><strong>Takeaways</strong></p><p><strong>Review code before you install it.</strong> Especially for tools that want filesystem access, API keys, or the ability to run commands. “It’s open source” doesn’t mean “it’s been audited.” NanoClaw is 3,500 lines — genuinely readable. Most people would just run `/setup` and trust it.</p><p><strong>Read the security model, then check if the code matches.</strong> NanoClaw’s `SECURITY.md` is excellent. It told me exactly what the developers consider threats, what the boundaries are supposed to be, and where to look for gaps. The vulnerability was literally a gap between the documented model and the implementation.</p><p><strong>Follow the data, not the code.</strong> I didn’t find this by reading every function top-to-bottom. I picked a specific untrusted input (`folder` from a WhatsApp-triggered MCP tool) and traced it through every function it touched, asking at each step: “is this validated?” Nine steps. Zero validation.</p><p><strong>Defense in depth means checking at every layer.</strong> The mount security module is great — but it protects one category of mounts. The core mounts assumed they’d always receive safe input. That assumption is the vulnerability.</p><p><strong>Small codebases are auditable.</strong> This is NanoClaw’s real advantage over larger alternatives. I could read and understand the entire security surface in an afternoon. Try doing that with a 52-module system.</p><p>— -</p><p>Rafael Martinez is a Cloud Security Engineer. Building cybersecurity tools, AI-powered products, and digital resources at <a href="https://securecloudacademy.gumroad.com/">https://securecloudacademy.gumroad.com/</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d5c6a167b76b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI Agents Are Becoming Infrastructure — So I Built My Own Vault]]></title>
            <link>https://medium.com/@terminalsandcoffee/ai-agents-are-becoming-infrastructure-so-i-built-my-own-vault-aefe40b8f4e6?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/aefe40b8f4e6</guid>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[cloud-security]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Mon, 16 Feb 2026 16:19:01 GMT</pubDate>
            <atom:updated>2026-02-16T16:19:01.168Z</atom:updated>
            <content:encoded><![CDATA[<p>A hardened Terraform template for running OpenClaw (or any AI agent) on zero-trust infrastructure with Tailscale VPN, kernel hardening, and nine security layers—because AI agents hold your API keys, and the attack surface is real.</p><p><strong>If your agent can browse the web, the web can browse your agent.</strong></p><p><strong>That’s not a hypothetical threat—it’s the reality of running autonomous AI agents in 2026. These agents execute code, hold API keys, manage your email, and make purchases on your behalf. They’re high-value targets sitting on the open internet. And if you’re running yours on a bare EC2 instance with port 22 open to the world, you’re one exploit away from a very bad day.</strong></p><p><strong>When Peter Steinberger (creator of OpenClaw) joined OpenAI, it confirmed what security engineers already knew: autonomous agents are becoming infrastructure. And infrastructure must be locked down from the very first terraform apply.</strong></p><p><strong>What is Openclaw?</strong></p><p>OpenClaw is an agentic AI interface that:</p><p>Which means it runs locally on your own hardware (Mac Mini, VPS, Raspberry Pi, EC2 instances, etc.) Features voice assistant, browser automation, home automation, and cron scheduling</p><p>Peter Steinberger built OpenClaw — an open-source AI agent that does real things: buys cars, clears your inbox, checks in for flights while you sleep. It hit 180,000 GitHub stars. And as of today, <a href="https://techcrunch.com/2026/02/15/openclaw-creator-peter-steinberger-joins-openai/">Steinberger is joining OpenAI</a>.</p><p>The project moves forward. Altman says it’ll become “core to our product offerings.” Translation: this technology is too important to leave on the table.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m2r4OAeSyQnAAEKdXEZ7hg.png" /></figure><p>Why This Matters to You</p><p>Think about what your agent has access to:</p><p>Your email inbox — it reads messages, knows who you correspond with, and could expose confidential business communications if compromised.</p><p>Your API keys — Stripe for payments, AWS for infrastructure, GitHub for code. An attacker doesn’t need to break into these services directly. They just need to compromise your agent.</p><p>Your calendar and contacts — meeting links, private conversations, client information. All sitting in memory on a server somewhere.</p><p>The risk isn’t theoretical. AI agents are executing real transactions with real money. They’re modifying production infrastructure. They’re trusted components of your workflow. That makes them valuable attack surfaces.</p><p>Here’s what that means for the rest of us: AI agents that operate autonomously on your infrastructure aren’t a novelty anymore. They’re becoming the default. And if you’re an engineer running agents on a bare EC2 instance with port 22 open to the world, this could cause a few problems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uOXdOkeBaUJTXbxUriNKAw.png" /></figure><p>So I built the infrastructure first.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XzrkAaz-2pJEviswhTC8cQ.png" /></figure><p><strong>Why I Built OpenClaw Vault</strong></p><p><a href="https://github.com/TerminalsandCoffee/openclaw-vault">OpenClaw Vault</a> is a Terraform template that deploys a hardened Ubuntu 24.04 LTS instance on AWS with zero public attack surface. No open ports. No SSH exposed to the internet. Access is exclusively through Tailscale VPN.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JPFjc0BP5DAdLnlvFYoc0A.png" /></figure><p>The reasoning is simple: if you’re going to run an AI agent that can browse the web, execute code, manage your email, and interact with external APIs on your behalf — the machine it runs on needs to be locked down. Not “I’ll add security later” locked down. Locked down from the first.</p><pre>terraform apply</pre><p>The instance provisions with nine hardening layers automatically:</p><p>1. <strong>Tailscale mesh VPN</strong> — the only way in. No public SSH, no open security group rules.</p><p>2. <strong>SSH hardened</strong> — port 2222, root login disabled, password auth disabled, 3 max auth tries, 30-second login grace.</p><p>3. <strong>UFW firewall</strong> — default deny inbound, SSH allowed only on the `tailscale0` interface.</p><p>4. <strong>Kernel hardening</strong> — reverse-path filtering, SYN cookies, ASLR, ICMP redirect blocking, IPv6 disabled.</p><p>5. <strong>IMDSv2 enforced</strong> — prevents SSRF attacks from stealing instance metadata credentials.</p><p>6. <strong>fail2ban</strong> — 1-hour ban after 3 failed SSH attempts.</p><p>7. <strong>auditd</strong> — watches auth logs, passwd/shadow, sudoers, SSH config, cron, and network config changes.</p><p>8. <strong>Automatic security updates</strong> — `unattended-upgrades` pulls security patches daily.</p><p>9. <strong>Encrypted EBS</strong> — root volume encrypted at rest with AWS-managed keys.</p><p>The whole thing deploys with</p><pre>`terraform init &amp;&amp; terraform apply`<br>Three inputs: your AWS region, instance type, and a Tailscale auth key.</pre><p><strong>The OpenAI Acquisition Changes the Calculation</strong></p><p>When OpenClaw was an indie project burning $10–20K a month from Steinberger’s pocket, self-hosting was optional. Enthusiasts ran it locally. Most people just talked about it.</p><p>Now it’s backed by OpenAI’s compute, distribution, and engineering resources. The <a href="https://www.cnbc.com/2026/02/15/openclaw-creator-peter-steinberger-joining-openai-altman-says.htm">agent will be integrated into ChatGPT’s product line</a>. The foundation will keep it open source, but the gravity shifts toward hosted offerings. That’s the play — give OpenAI the distribution, keep the code open.</p><p>This matters for engineers because the question is no longer “should I experiment with AI agents?” It’s “where do I run them?”</p><p>You have two options: let someone else host your autonomous agent (and the data it touches, the APIs it accesses, the credentials it holds) — or run it yourself on infrastructure you control. I’d rather control the infrastructure.</p><p><strong>Build Your Own or Fork Mine</strong></p><p>If you want to get hands-on, you have two paths:</p><p><strong>Fork OpenClaw Vault and customize it.</strong> The <a href="https://github.com/TerminalsandCoffee/openclaw-vault">repo is MIT-licensed</a>. Clone it, add your Tailscale auth key, and you’ve got a hardened instance ready for whatever agent stack you choose — OpenClaw, Claude Code, your own custom setup.</p><pre>git clone https://github.com/TerminalsandCoffee/openclaw-vault.git<br>cp terraform.tfvars.example terraform.tfvars</pre><p>Add your tailscale_auth_key</p><pre>terraform init &amp;&amp; terraform apply</pre><p>After deployment, connect via Tailscale SSH:</p><pre>ssh openclaw</pre><p>No keys to manage. Identity-based auth through your tailnet.</p><p><strong>Build your own from scratch with AI agents.</strong></p><p>This is the path I actually recommend.</p><p>I co-authored this entire infrastructure template with Claude Opus 4.6. Every Terraform file, every hardening rule in the userdata script, every sysctl parameter — pair-programmed with an AI agent.</p><p>That’s the real skill here. Not memorizing sysctl flags. Knowing how to direct an AI agent to produce production-grade infrastructure, then validating every decision it makes. If you can do that, you can build anything in this space.</p><p><strong>What’s Next: Cost Optimization and Deeper Security</strong></p><p>The current template runs a `t3.micro` — free tier eligible, roughly $11–$14/month if you’re paying. That’s fine for a personal agent server. But there’s room to optimize:</p><p><strong>Cost optimization on the roadmap:</strong></p><p>- <strong>Spot instances</strong> — AI agent workloads are interruptible. A spot `t3.micro` cuts costs 60–70%.</p><p>- <strong>Scheduled scaling</strong> — if your agent only runs during business hours, schedule stop/start with EventBridge.</p><p>- <strong>ARM instances</strong> — `t4g.micro` on Graviton is cheaper and faster for most workloads.</p><p>- <strong>S3 backend for Terraform state</strong> — currently local. Moving to S3 with DynamoDB locking enables team collaboration and state recovery.</p><p><strong>Additional security layers planned:</strong></p><p>- <strong>CrowdSec</strong> — community-driven intrusion detection. Block IPs that other CrowdSec users have flagged as malicious.</p><p>- <strong>OSSEC/Wazuh</strong> — host-based intrusion detection with file integrity monitoring.</p><p>- <strong>AppArmor profiles</strong> — confine the agent process to only the system calls and file paths it needs.</p><p>- <strong>Network segmentation</strong> — private subnet with NAT gateway instead of public subnet. The instance shouldn’t have a public IP at all.</p><p>- <strong>Secrets Manager integration</strong> — API keys and agent credentials stored in AWS Secrets Manager, not environment variables.</p><p>- <strong>CloudWatch alarms</strong> — alert on unusual CPU, network, or API call patterns that might indicate agent compromise.</p><p><strong>The Skill Layer: Why SKILL.md Matters</strong></p><p>If you’re running OpenClaw (or any agent framework with a similar pattern), the real power isn’t the base model. It’s the <strong>skills layer</strong>.</p><p>In OpenClaw, a <a href="https://docs.openclaw.ai/tools/skills">skill</a> is a folder with a `SKILL.md` file — plain Markdown that teaches the agent how to perform a specific task. Skills live in</p><pre>~/.openclaw/workspace/skills/&lt;skill&gt;/SKILL.md. </pre><p>There are over 5,700 community-contributed skills on <a href="https://github.com/VoltAgent/awesome-openclaw-skills">ClawHub</a> right now.</p><p>Here’s why this matters:</p><p><strong>Skills are progressive disclosure.</strong> OpenClaw doesn’t load every skill into context at startup — that would burn tokens and confuse the model. It loads the name and description. Only when a task matches does the agent read the full SKILL.md instructions. This is efficient prompt engineering at the framework level.</p><p><strong>Skills are composable.</strong> A “deploy to AWS” skill can call a “run Terraform” skill, which can call a “validate HCL” skill. You build complex agent workflows from simple, testable units.</p><p><strong>Skills are the moat.</strong> The base model is the same for everyone. OpenAI, Anthropic, whoever — the model weights are the foundation. But the skills you write, the workflows you compose, the domain knowledge you encode in SKILL.md files — that’s yours. That’s what makes your agent setup unique and valuable.</p><p><strong>Skills are portable.</strong> Because they’re plain Markdown files in a directory, they’re version-controlled, shareable, and framework-agnostic in principle. The pattern of “structured instructions that an AI agent reads at runtime” isn’t exclusive to OpenClaw. Claude Code has a similar concept with its skills system. This pattern will become standard.</p><p>If you’re an engineer building with AI agents, start writing skills. Not just using them — writing them. Encode your domain expertise into structured agent instructions. That’s the skill that compounds.</p><p><strong>The Bigger Picture</strong></p><p>Steinberger joining OpenAI is a signal. The infrastructure layer for AI agents is consolidating. Open source agents are being absorbed into platform companies. The engineers who understand how to self-host, secure, and extend these systems — rather than just consume them — are the ones who’ll have leverage.</p><p>OpenClaw Vault is a small piece of that: a hardened, reproducible, single-command deployment for running whatever agent you choose on infrastructure you own.</p><p>Fork it, break it, rebuild it. That’s how you learn.</p><p>Get started with OpenClaw Vault at github.com/TerminalsandCoffee/openclaw-vault.</p><p>The entire infrastructure template is MIT-licensed and ready to deploy. If you’re building with AI agents and want to stress-test the security model, reach out—I’m actively looking for feedback from engineers running agents in production.f</p><p>— -</p><p><em>Rafael Martinez is a Cloud Security Engineer. Building security and sharing tools while also shipping cybersecurity guides at </em>https://<em>securecloudacademy</em>.gumroad.com.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aefe40b8f4e6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Your Resume Doesn’t Work Anymore]]></title>
            <link>https://medium.com/@terminalsandcoffee/your-resume-doesnt-work-anymore-36d1bc80fc74?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/36d1bc80fc74</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[resume]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Sun, 15 Feb 2026 23:06:42 GMT</pubDate>
            <atom:updated>2026-02-15T23:06:42.361Z</atom:updated>
            <content:encoded><![CDATA[<p>Here’s the truth nobody in the career advice space wants to say out loud: your resume gets 6 seconds. Six. A recruiter glances at it, pattern-matches<br>against the job description, and moves on. You could be the perfect candidate and still get filtered out because a keyword was missing or your formatting tripped up the ATS.</p><p>Meanwhile, every candidate is using the same templates, the same action verbs, the same “Results-driven professional with X years of experience” opener. Everyone looks the same on paper. So I built something different.</p><p><strong>What I Built</strong></p><p>I created a portfolio site where recruiters can ask an AI questions about me — and get real, accurate answers in real-time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1011/1*FtvSljENGxjDnI3FRimvJg.png" /><figcaption><a href="https://rafs-ai-resume.com/">https://rafs-ai-resume.com/</a></figcaption></figure><p>Not a chatbot with canned responses. An actual AI assistant trained on my professional background, skills, experience, and what roles I’m a strong (or<br>weak) fit for. It knows my tech stack, my certifications, my project history and it answers honestly.</p><p>It also has a Fit Check feature: a recruiter can paste a job description, and the AI gives a candid analysis of how well I match the role. Not hype. Not<br>spin. An honest breakdown — strengths, gaps, and all.</p><p>The whole site — animated skill bars, experience cards, dark theme — runs on React, TypeScript, and Tailwind. The AI backend is a Vercel Edge Function hitting OpenAI. <strong>Cost per conversation</strong>: about half a penny.</p><p><strong>Why This Matters Right Now</strong></p><p>We’re in the middle of a fundamental shift. AI isn’t replacing jobs — it’s replacing people who don’t use AI. The candidates who stand out in 2026 aren’t the ones with the longest resume. They’re the ones who demonstrate they can actually build with modern tools.</p><p>An AI-powered resume site does two things at once:</p><p>1. It shows your skills better than a PDF ever could. A recruiter doesn’t have to guess whether you can build real applications. They’re literally using<br> one.<br> 2. It signals that you understand where tech is going. If you’re applying for any role that touches software, data, or cloud — showing up with an<br> interactive AI portfolio puts you in a different category than everyone else submitting the same Word doc.</p><p>Not a gimmick. A functional demo of your capabilities disguised as a resume.</p><p><strong>Why I Built It as a Template</strong></p><p>After I deployed my own version, people started asking how I did it. The answer was always the same: “It’s actually not that complicated, you just need to know what to put where.”</p><p>So I packaged the whole thing into a template.</p><p>You edit two files — one for your resume content, one for your AI personality — and deploy. That’s it. No wrestling with APIs, no frontend framework expertise required.</p><p>I open-sourced it because I believe everyone should have access to tools that level the playing field. But I also know that not everyone has the time or interest to set it up themselves.</p><p><strong>Three Ways to Get Your Own</strong></p><p><strong>Option 1: Build It Yourself with AI</strong></p><p>If you’re technical (or want to learn), you can absolutely build something like this from scratch. Open up Claude, Loveable or whatever AI coding agent you prefer and start prompting. Here’s a starting point:</p><p>“Build me a React portfolio site with an AI chat feature where recruiters can ask questions about my professional background. Include animated skill bars, experience cards, and a job fit analysis tool. Use Vite, Tailwind CSS, and Vercel Edge Functions for the AI backend.”</p><p>Iterate from there. AI agents are shockingly capable at scaffolding full projects now — you just need to guide them with good prompts and review the output.</p><p><strong>Option 2: Grab My Template</strong></p><p>Don’t want to start from zero? I packaged everything into a ready-to-deploy template. Edit 2 files with your info, click deploy, and you’re live in under<br> an hour.</p><p><a href="https://github.com/TerminalsandCoffee/ai-resume-template">GitHub - TerminalsandCoffee/ai-resume-template</a></p><p>What’s included:<br> — Full React + TypeScript site with polished dark theme<br> — AI chat powered by OpenAI (recruiters ask, your AI answers)<br> — Job fit analysis feature<br> — Animated skill bars, certifications, experience cards<br> — One-click Vercel deploy<br> — Two-file customization (your content + your AI personality)<br> — MIT licensed — make it yours</p><p><strong>Option 3: Hire Me to Build It For You — $499</strong></p><p>Want a fully customized AI resume site without touching a line of code? I’ll build it for you.</p><p>Here’s what you get:<br> — Custom design tailored to your field and personal brand<br> — AI personality written and tuned to represent you accurately<br> — All your experience, skills, and certifications populated<br> — Deployed and live on your own domain<br> — One round of revisions after delivery</p><p>This is for professionals who know the value of standing out but would rather invest money than time. You send me your resume, we hop on a quick call, and I deliver a live site within a week.</p><p><strong>The Bottom Line</strong></p><p>The job market rewards people who refuse to blend in. A PDF resume is table stakes. An AI-powered portfolio site is a competitive advantage.</p><p>Whether you build it yourself, use my template, or hire me to do it — the move is the same: stop being a piece of paper and start being an experience.</p><p>— -<br> Rafael builds cybersecurity tools, AI-powered products, and digital resources at <a href="https://securecloudacademy.gumroad.com/">https://securecloudacademy.gumroad.com/</a>.</p><p>Follow for more on AI, security, and building things that stand out in 2026.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=36d1bc80fc74" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built a Security Proxy for LLM APIs]]></title>
            <link>https://medium.com/@terminalsandcoffee/i-built-a-security-proxy-for-llm-apis-8c44f7c26730?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/8c44f7c26730</guid>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Sat, 14 Feb 2026 16:00:55 GMT</pubDate>
            <atom:updated>2026-02-14T16:00:55.385Z</atom:updated>
            <content:encoded><![CDATA[<h4>Here’s What I Learned</h4><p>Every company is racing to ship AI features. Most are connecting directly to OpenAI or Bedrock, handing API keys to application teams, and hoping for the best.</p><p>That’s a problem. There’s no visibility into what’s being sent, no guardrails on what comes back, and no audit trail when something goes wrong.</p><p>So I built one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BAcVi48xkteA89EBMt1sVA.png" /></figure><h3>The Problem</h3><p>When your application talks directly to an LLM API, you have no control over:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*yLaw5MbAGVb0iHQ5wlcGVw.png" /></figure><p>The standard answer is “we’ll add that later.” The realistic answer is that it never gets added, and the first time someone pastes a customer’s SSN into GPT-4, you find out from your compliance team.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HvxuUIE2paH1ismQkxzrKw.png" /></figure><h3>The Solution: A Security Proxy</h3><p>The LLM Security Gateway sits between your application and the LLM API. It mirrors the OpenAI /v1/chat/completions endpoint, so your app doesn&#39;t need to change — just point the base URL at the gateway instead of OpenAI directly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GUkV2Sy2CHJTWaqh81PThQ.png" /></figure><p>Every request passes through an 8-stage security pipeline before it reaches the LLM, and the response gets scanned before it reaches the client.</p><h3>The Pipeline</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JmIfmv-X6XMM-y3czZL1CQ.png" /></figure><ol><li>Authentication — each client gets its own API key with per-client config</li><li>Rate limiting — sliding window per client, configurable RPM</li><li>Model allowlist — restrict which models each client can access</li><li>Prompt injection detection — 20 regex patterns across 4 categories (instruction override, role manipulation, delimiter injection, context manipulation) with cumulative risk scoring</li><li>PII scanning — SSN, credit card (Luhn-validated), email, phone, IPv4. Configurable: redact the PII and forward, block the request entirely, or just log it</li><li>Forward to provider — routes to OpenAI or AWS Bedrock based on client config</li><li>Response scanning — the same injection and PII scanners run on the LLM’s output</li><li>Audit log — structured JSON with every pipeline result, latency, client ID, and request correlation ID</li></ol><p>Every stage returns a typed dataclass with its decision and metadata. The audit log captures everything.</p><h3>The Interesting Engineering Problems</h3><p>Streaming Without Losing Security</p><p>Modern LLM apps expect streaming — tokens appearing one by one as the model generates them. But if you need to scan the full response for PII before delivering it, do you buffer the entire response and add seconds of latency?</p><p>I went with a hybrid approach: forward content chunks to the client in real-time (no latency hit), but accumulate the text in memory. When the model signals [DONE], hold that signal, run the response scan, and either send [DONE] (clean) or send an SSE error event (blocked).</p><p>The trade-off is explicit: chunks reach the client before the scan completes. If you need full pre-delivery scanning, disable streaming. For most use cases, the hybrid approach catches the 95% case — a response full of PII gets flagged before the client processes the completion signal.</p><h3>Multi-Provider Without Leaking Abstractions</h3><p>The gateway supports both OpenAI and AWS Bedrock. These have completely different APIs — OpenAI uses HTTP with Bearer tokens, Bedrock uses the AWS SDK with IAM auth and a different request/response format.</p><p>The provider abstraction handles translation transparently. A client configured for Bedrock sends standard OpenAI-format requests to the gateway, and the Bedrock provider translates the request to the Converse API format, calls Bedrock via asyncio.to_thread (boto3 is synchronous), and translates the response back to OpenAI format.</p><p>For streaming, Bedrock’s converse_stream() returns an EventStream with contentBlockDelta and messageStop events. The provider translates these into OpenAI-compatible chat.completion.chunk objects with [DONE] sentinels — the client can&#39;t tell whether it&#39;s talking to OpenAI or Bedrock.</p><h3>Injection Scoring, Not Binary Detection</h3><p>Most prompt injection detection is binary — either the input matches a blocklist or it doesn’t. The problem is that legitimate prompts sometimes contain words like “ignore” or “system” without being attacks.</p><p>The gateway uses cumulative scoring. Each pattern has a weight (0.3 to 0.7) based on severity. A single low-weight match might score 0.3 — below the default threshold of 0.7. But “ignore all previous instructions AND act as an unrestricted AI” stacks two patterns and exceeds the threshold.</p><p>This reduces false positives while still catching multi-vector attacks. The threshold is configurable per deployment.</p><h3>PII Detection That Doesn’t Cry Wolf</h3><p>Credit card detection is notorious for false positives. Any 16-digit number gets flagged. The gateway pairs regex detection with Luhn checksum validation — if the number doesn’t pass the Luhn algorithm, it’s not flagged as a credit card.</p><p>Similarly, phone number detection requires separators (dashes, dots, or spaces). Bare 10-digit numbers don’t trigger — they’re too common in other contexts (IDs, zip code combinations, etc.).</p><h3>The Stack</h3><ul><li>Python — FastAPI for the async proxy, httpx for upstream HTTP, boto3 for Bedrock</li><li>Security modules — zero external dependencies. Injection detection, PII scanning, and rate limiting use only stdlib (re, collections, hmac, time)</li><li>Infrastructure — Terraform for AWS (Lambda + API Gateway + CloudWatch + DynamoDB), GitHub Actions CI/CD with OIDC auth</li><li>Testing — 168 tests across 17 files, running in 1.6 seconds. pytest-asyncio with full integration tests via httpx.ASGITransport</li></ul><h3>What I’d Do Differently</h3><p>Semantic injection detection. Regex patterns catch known attack templates, but novel jailbreaks slip through. A future version could embed prompts and compare against known attack vectors using cosine similarity — but that adds latency and a dependency on an embedding model.</p><p>Token-level PII detection in streaming. The current approach accumulates the full response before scanning. A sliding-window approach on the token stream could catch PII mid-generation, but the complexity of partial-match detection across chunk boundaries isn’t worth it for most use cases.</p><p>Admin API. Right now, client config is file-based or DynamoDB. An authenticated admin API for CRUD operations on clients would be useful for larger deployments, but I deliberately avoided it to minimize attack surface.</p><h3>Try It</h3><p>The project is open source: <a href="https://github.com/TerminalsandCoffee/llm-security-gateway">github.com/TerminalsandCoffee/llm-security-gateway</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xhKKOLwk0YDfozePT1-V9A.png" /></figure><p>Clone it, run</p><pre>uvicorn src.main:app --reload</pre><p>and point your OpenAI SDK at http://localhost:8000.</p><p>Your existing code works unchanged — but now every request is authenticated, rate-limited, scanned for injection and PII, and logged.</p><p><em>Rafael Martinez is a Cloud Security Engineer and creator of Terminals and Coffee. He builds security tools and ships cybersecurity guides at </em><a href="https://terminalsandcoffee.gumroad.com/"><em>terminalsandcoffee.gumroad.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8c44f7c26730" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Cloud-Native Detection Engineering Lab with Terraform and AWS]]></title>
            <link>https://medium.com/@terminalsandcoffee/building-a-cloud-native-detection-engineering-lab-with-terraform-and-aws-63d3990190f1?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/63d3990190f1</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[clawdbot]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[detection-engineering]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Mon, 26 Jan 2026 17:39:43 GMT</pubDate>
            <atom:updated>2026-01-26T17:39:43.997Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How a RAM bottleneck turned into a fully automated, repeatable security lab</em></p><h3>The Problem</h3><p>I was taking a detection engineering course that relied on local virtual machines — Kali Linux for offence, Windows and Ubuntu as the target, and an ELK stack for analysis.</p><p>Solid approach. One issue though: running all of that locally requires more RAM than my laptop could realistically handle.</p><p>Rather than fight hardware limits, I moved the entire lab to AWS and rebuilt it using Terraform with the help from Clawdbot.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*GwNTlDz-6RMZPP6gqYsGfA.png" /></figure><p>This was such a fun tool to work with. However, I did end up uninstalling it after since its such a new tool and who knows the security implications that may start to arise.</p><p>The result: a fully automated detection engineering environment — attacker, victim, victim, and SIEM — deployed with a single terraform apply.</p><p>No manual installs. No fragile VM snapshots. Just infrastructure as code.</p><h3>What We’re Building</h3><p>A complete detection engineering lab composed of three EC2 instances:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/813/1*YsfZP7AFj4G2xbRdmoafxA.png" /></figure><p>The key idea: infrastructure, telemetry, and log flow are all <strong>code-defined and reproducible</strong>. Tear it down, spin it back up, and you’re hunting again in minutes.</p><h3>The Architecture</h3><p>The lab is intentionally simple but mirrors real-world detection pipelines: attack → telemetry → centralized analysis.</p><pre>┌─────────────────────────────────────────────────────────────┐<br>│                        AWS VPC                              │<br>│                                                             │<br>│   ┌─────────────┐    attack    ┌─────────────────────┐     │<br>│   │             │ ──────────── │                     │     │<br>│   │  Kali Linux │              │   Windows Server    │     │<br>│   │  (Attacker) │              │   - Sysmon          │     │<br>│   │             │              │   - Winlogbeat      │     │<br>│   └─────────────┘              └──────────┬──────────┘     │<br>│                                           │ logs           │<br>│                                           ▼                │<br>│                                ┌─────────────────────┐     │<br>│                                │   Ubuntu (Elastic)  │     │<br>│                                │   - Elasticsearch   │     │<br>│                                │   - Kibana          │     │<br>│                                └─────────────────────┘     │<br>│                                           │                │<br>└───────────────────────────────────────────┼────────────────┘<br>                                            │<br>                                            ▼<br>                                    You, in Kibana,<br>                                    writing detections</pre><h3>Prerequisites</h3><p>Before getting started, you’ll need:</p><ul><li><strong>AWS account</strong> with credentials configured (aws configure)</li><li><strong>Terraform</strong> installed</li><li><strong>AWS key pair</strong> for SSH/RDP access</li><li><strong>Security group</strong> (intentionally permissive for lab use)</li></ul><h3>Step 1: Terraform Project Structure</h3><pre>detection-engineering/<br>├── setup/<br>│   └── terraform/<br>│       ├── main.tf<br>│       ├── variables.tf<br>│       └── terraform.tfvars<br>└── detections/</pre><p>This structure keeps infrastructure and detection logic separate and version-controlled.</p><h3>Step 2: Define Variables</h3><p><strong>variables.tf</strong></p><pre>variable &quot;aws_region&quot; {<br>  type    = string<br>  default = &quot;us-east-1&quot;<br>}</pre><pre>variable &quot;kali_ami&quot; {<br>  type = string<br>}</pre><pre>variable &quot;windows_ami&quot; {<br>  type = string<br>}</pre><pre>variable &quot;ubuntu_ami&quot; {<br>  type = string<br>}</pre><pre>variable &quot;security_group_id&quot; {<br>  type = string<br>}</pre><pre>variable &quot;key_name&quot; {<br>  type = string<br>}</pre><p><strong>terraform.tfvars</strong></p><pre>aws_region        = &quot;us-east-1&quot;<br>kali_ami          = &quot;ami-09e99f75cc7592017&quot;<br>windows_ami       = &quot;ami-06b5375e3af24939c&quot;<br>ubuntu_ami        = &quot;ami-0ecb62995f68bb549&quot;<br>security_group_id = &quot;sg-xxxxxxxxxxxxxxxxx&quot;<br>key_name          = &quot;your-key-pair-name&quot;</pre><h3>Step 3: Main Infrastructure</h3><p>Each instance is bootstrapped using user_data so the environment configures itself on first launch.</p><p>No SSH-and-pray.</p><h3>Kali Linux (Attacker)</h3><pre>resource &quot;aws_instance&quot; &quot;kali_linux&quot; {<br>  ami                    = var.kali_ami<br>  instance_type          = &quot;t2.medium&quot;<br>  vpc_security_group_ids = [var.security_group_id]<br>  key_name               = var.key_name<br><br>  tags = {<br>    Name = &quot;Kali Linux&quot;<br>  }<br>}</pre><h3>Elastic Stack (SIEM)</h3><p>Elastic requires memory, so this instance is intentionally sized larger.</p><pre>resource &quot;aws_instance&quot; &quot;ubuntu_vm&quot; {<br>  ami           = var.ubuntu_ami<br>  instance_type = &quot;t2.large&quot;<br><br>  user_data = &lt;&lt;-EOF<br>  #!/bin/bash<br>  set -e<br>  sleep 30<br><br>  apt-get update<br>  apt-get install -y curl apt-transport-https<br><br>  curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | \<br>    gpg --dearmor -o /usr/share/keyrings/elastic.gpg<br><br>  echo &quot;deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main&quot; \<br>    &gt; /etc/apt/sources.list.d/elastic.list<br><br>  apt-get update<br>  apt-get install -y elasticsearch kibana<br><br>  sed -i &#39;s/#network.host: .*/network.host: 0.0.0.0/&#39; /etc/elasticsearch/elasticsearch.yml<br>  sed -i &#39;s/#discovery.type:.*/discovery.type: single-node/&#39; /etc/elasticsearch/elasticsearch.yml<br>  echo &quot;xpack.security.enabled: false&quot; &gt;&gt; /etc/elasticsearch/elasticsearch.yml<br><br>  systemctl enable elasticsearch kibana<br>  systemctl start elasticsearch kibana<br>  EOF<br>}</pre><h3>Windows Server (Target)</h3><p>Windows installs Sysmon and Winlogbeat automatically and begins shipping logs to Elastic.</p><pre>resource &quot;aws_instance&quot; &quot;windows_server&quot; {<br>  ami           = var.windows_ami<br>  instance_type = &quot;t2.medium&quot;<br><br>  depends_on = [aws_instance.ubuntu_vm] # Ensure Elastic is ready<br><br>  user_data = &lt;&lt;-EOF<br>  &lt;powershell&gt;<br>  Invoke-WebRequest https://download.sysinternals.com/files/Sysmon.zip -OutFile Sysmon.zip<br>  Expand-Archive Sysmon.zip<br>  Invoke-WebRequest https://raw.githubusercontent.com/SwiftOnSecurity/sysmon-config/master/sysmonconfig-export.xml -OutFile sysmon.xml<br>  .\Sysmon\Sysmon64.exe -accepteula -i sysmon.xml<br>  &lt;/powershell&gt;<br>  EOF<br>}</pre><h3>Step 4: Security Group Rules</h3><p>Inbound rules required for the lab:</p><pre>| Port | Purpose       |<br>| ---- | ------------- |<br>| 22   | SSH           |<br>| 3389 | RDP           |<br>| 5601 | Kibana        |<br>| 9200 | Elasticsearch |</pre><p>⚠️ <strong>Lab Tradeoff:</strong> These rules are intentionally open for ease of access. In a real environment, you would restrict ingress and never expose Elasticsearch publicly.</p><h3>Step 5: Deploy</h3><pre>terraform init<br>terraform plan<br>terraform apply</pre><p>Within ~10 minutes, the full lab is online and generating telemetry.</p><h3>Step 6: Start Hunting</h3><ul><li>SSH into <strong>Kali</strong> (attacker)</li><li>RDP into <strong>Windows</strong> (target)</li><li>Open <strong>Kibana</strong> (SIEM)</li></ul><h3>Simulate activity</h3><pre>nmap -sV &lt;windows-private-ip&gt;</pre><h3>Observe telemetry</h3><p>In Kibana:</p><ul><li>Create index pattern: winlogbeat-*</li><li>Search:</li></ul><pre>event.code:1 OR process.name:nmap</pre><h3>Example Detection Rule</h3><p>Here’s a simple Elastic rule to validate detection logic:</p><pre>[rule]<br>name = &quot;Nmap Network Scan Detected&quot;<br>type = &quot;query&quot;<br>query = &quot;process.name:nmap OR process.command_line:*nmap*&quot;<br>severity = &quot;medium&quot;<br>risk_score = 50<br><br>[[rule.threat]]<br>framework = &quot;MITRE ATT&amp;CK&quot;<br>[[rule.threat.technique]]<br>id = &quot;T1046&quot;<br>name = &quot;Network Service Discovery&quot;</pre><h3>Cleanup</h3><p>When finished:</p><pre>terraform destroy</pre><p>Everything is removed. No lingering costs.</p><h3>What’s Next?</h3><ul><li>Install Atomic Red Team on Windows</li><li>Sync detections via GitHub Actions</li><li>Convert Sigma rules to Elastic format</li><li>Add “Detection-as-Code” (DaC) Components</li><li>Implement a “Detection Lifecycle” Documentation</li><li>So much room for activities!</li></ul><h3>Final Thoughts</h3><p>In today’s forever evolving by the minute world, you don’t need a maxed-out laptop to learn detection engineering. You can use AI to help you innovate something new.</p><p>With Terraform and AWS, you can spin up a realistic, cloud-native lab in minutes, tear it down when you’re done, and version-control the entire environment.</p><p>Infrastructure as Code isn’t just for DevOps — it’s a force multiplier for security engineers who want repeatability, realism, and speed.</p><p>Build the lab.<br>Break it.<br>Detect it.<br>Automate it.</p><p>Then do it again.</p><p>You can check out the repo here:</p><p><a href="https://github.com/TerminalsandCoffee/detection-engineering">GitHub - TerminalsandCoffee/detection-engineering: This is a public repo for gaining knowledge and hands on experience for detection engineering fundamentals</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=63d3990190f1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Frameworks for Cyber Security Fundamentals]]></title>
            <link>https://medium.com/@terminalsandcoffee/frameworks-for-cyber-security-fundamentals-5721a2a44aad?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/5721a2a44aad</guid>
            <category><![CDATA[cloud-security]]></category>
            <category><![CDATA[threat-intelligence]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[incident-response]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Thu, 18 Dec 2025 13:46:49 GMT</pubDate>
            <atom:updated>2025-12-18T13:46:49.744Z</atom:updated>
            <content:encoded><![CDATA[<p>As promised, most of my writing moving forward will live at the intersection of cybersecurity and cloud security.</p><p>As I prepare to step into a new role as a Threat and Vulnerability Analyst, I’ve been revisiting the fundamentals not because they’re basic, but because they shape how to structure you’re thinking.</p><p>Before alerts, dashboards, and tooling, there are mental models.</p><p>Three in particular have been front and center in my review:</p><ul><li><strong>The Cyber Kill Chain</strong></li><li><strong>The MITRE ATT&amp;CK Framework</strong></li><li><strong>F3EAD</strong></li></ul><p>Each one answers a different question, and together they form a powerful way to move from reactive alerting to adversary-focused thinking.”</p><ul><li><strong>Cyber Kill Chain</strong> — <em>Where am I in the attack?</em></li></ul><p>The Kill Chain helps me quickly ask: “is this noise, or is this progression?”</p><p>The Cyber Kill Chain is a framework introduced by Lockheed Martin in 2011 to model how cyber attacks unfold. It breaks adversary activity into seven sequential stages, each representing a step an attacker typically takes as an intrusion progresses.</p><p>Understanding <em>where</em> an attacker is in the chain helps prioritize response and identify opportunities to disrupt the attack before it advances further.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/711/0*4RWHMDmbpkRk2nAR.png" /><figcaption><a href="https://www.lockheedmartin.com/en-us/capabilities/cyber/cyber-kill-chain.html">https://www.lockheedmartin.com/en-us/capabilities/cyber/cyber-kill-chain.html</a></figcaption></figure><ul><li><strong>MITRE ATT&amp;CK — <em>What is the adversary actually doing?</em></strong></li></ul><p>MITRE turns alerts into behavior, and behavior is harder to lie about.</p><p>MITRE ATT&amp;CK focuses on tactics, techniques, and procedures (TTPs). Because it emphasizes how attackers operate rather than the tools they use, it aligns with the top of the Pyramid of Pain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*V2xsu2w5XTNp5T3R" /></figure><p>ATT&amp;CK — short for Adversarial Tactics, Techniques, and Common Knowledge — provides a structured way to analyze cyber attacks as coordinated patterns of behavior rather than isolated events. Each stage of an intrusion is mapped to specific techniques, allowing for deeper analysis and more consistent investigations.</p><p>For SOC analysts and threat-focused roles, ATT&amp;CK serves as a practical reference for what to detect and how to respond at each stage of an attack. Detections and mitigations can be mapped directly to specific techniques, making investigations more effective and repeatable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zNTEgq5YW3GAcY_8j50m_A.png" /><figcaption><a href="https://attack.mitre.org/">https://attack.mitre.org/</a></figcaption></figure><ul><li><strong>F3EAD </strong>— <em>Now what do we do with this information?</em></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/957/1*lxlZFbVwpr9pAHZRuFbnCg.png" /></figure><p>F3EAD addresses two key issues.</p><ul><li>First, intelligence cycles shouldn’t just lead to more intelligence — they should drive decisive incident response actions.</li><li>Second, operations shouldn’t end once an objective is achieved. The information gained during any response should feed back into a new intelligence cycle, allowing teams to learn from previous incidents and improve future detection and response.</li></ul><p>Closing the loop is what turns experience into advantage.</p><p>As I step into this role, I’m less interested in chasing alerts and more interested in understanding frameworks and processes.</p><p>Tools will change. Frameworks evolve.</p><p>But the ability to think clearly, map behavior, and feed lessons learned back into detection is what compounds over time.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5721a2a44aad" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Best Career Advice I Can Offer.]]></title>
            <link>https://medium.com/@terminalsandcoffee/the-best-career-advice-i-can-offer-45a46ae3d3d6?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/45a46ae3d3d6</guid>
            <category><![CDATA[personal-development]]></category>
            <category><![CDATA[self-image]]></category>
            <category><![CDATA[mental-health]]></category>
            <category><![CDATA[career-advice]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Mon, 15 Dec 2025 01:16:36 GMT</pubDate>
            <atom:updated>2025-12-15T01:16:36.329Z</atom:updated>
            <content:encoded><![CDATA[<p>Mental Health Awareness. A different kind of blog by terminals and coffee.</p><p>I often get messages on social media asking how to get a job in cloud engineering or cybersecurity. Some come from friends who are thinking about transitioning into tech. Others find me through my blogs or LinkedIn.</p><p>So instead of giving you another technical roadmap, certification list, or “learn AWS in 90 days” post, I want to share the most important non-technical advice I can offer — the kind you don’t hear often enough.</p><p>We all come from different backgrounds and cultures, so this may not apply to everyone. But it’s something I struggled with for a long time, and something I had to put an uncomfortable amount of effort into learning.</p><p>My best advice is this:</p><p>Learn how to heal your trauma — or more accurately, <strong>learn what it feels like to live with a calm nervous system.</strong></p><p>There were several moments over the last few years where my life looked like it was finally coming together.</p><p>I landed a much better job. I was being recognized at work. I started getting contract opportunities teaching cloud security — enough that I formed an LLC to bill through. From the outside, it looked like momentum. Progress. Proof that things were finally working.</p><p>So why would someone throw all of that away and start from scratch… again?</p><p>Strange, right?</p><p>What I eventually learned is that you can’t step into a new reality while your body is still protecting you from the old one.</p><p>It’s literally doing what its meant to do — protect you.</p><p>If your childhood, past relationships, or earlier environments were full of chaos, drama, or instability, your nervous system may have learned that chaos is normal. That emotional volatility, constant stress, or even abuse is just “how life works.”</p><p>But when things finally become calm — when relationships are healthy, work is stable, and opportunities arrive — that same calm can feel unfamiliar. Even threatening.</p><p>If you’re used to arguments, peace feels unsafe.<br>If you’re used to things falling apart, success feels temporary.<br>If you’re used to surviving, thriving feels uncomfortable.</p><p>And if you don’t recognize that pattern, you’ll unconsciously sabotage the very things you worked so hard to build.</p><p>So how do you heal trauma?</p><p>Honestly, I could write an entire series on what I’ve done over the last two years. And sometimes I’m not even a fan of the word “heal,” because to me, it’s not something you complete and move on from.</p><p>It’s more like getting in shape.</p><p>You don’t “finish” the gym. You don’t permanently arrive at healthy eating. You maintain it. You stay aware. If you stop, old patterns return.</p><p>Healing works the same way.</p><p>Here’s the stack — the practices that made the biggest difference for me:</p><ul><li>I began putting myself first, without guilt.</li></ul><p>I read <em>No More Mr. Nice Guy</em> by Robert Glover which helped point out a lot of things I needed to hear.</p><ul><li>I started meditating, reading, and journaling consistently (my top three books alone reshaped how I think).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2Pvap9vKmsmztqzYHdTBAQ.jpeg" /></figure><ul><li>Lost 60lbs and got into the best shape I ever been.</li><li>I stepped away from alcohol — and stayed away.</li><li>I got on a plane for the first time in 12 years.</li><li>Hired mentors (I can connect you with the two I worked with if you’re interested. They were the big brothers I’ve never had)</li><li>Found brotherhood and discipline in a local Muay Thai gym.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o8vDJiSP7qpE9bcYPF3G1Q.jpeg" /></figure><p>None of this made me perfect. None of it made life easy. But it made my nervous system steady enough to actually <strong><em>hold</em> the life I was building.</strong></p><p>So if you’re trying to break into tech — or level up your career — and you keep hitting invisible walls, burning out, or starting over for reasons you can’t quite explain… don’t just look at your resume.</p><p>Look at your <strong>internal state.</strong></p><p>Because skill will get you in the door.</p><p>But regulation, self-trust, and emotional stability are what let you stay — and grow.</p><p>That’s the best career advice I can offer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=45a46ae3d3d6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Terraform Walkthrough of Hybrid Routing]]></title>
            <link>https://medium.com/@terminalsandcoffee/a-terraform-walkthrough-of-hybrid-routing-18684a84150a?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/18684a84150a</guid>
            <category><![CDATA[web-server]]></category>
            <category><![CDATA[application-load-balancer]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Fri, 28 Nov 2025 01:29:41 GMT</pubDate>
            <atom:updated>2025-11-28T01:29:41.081Z</atom:updated>
            <content:encoded><![CDATA[<h3>with Linux, Windows &amp; Real-World Troubleshooting</h3><p>Application Load Balancers are the unsung heroes of modern cloud apps.<br>Everyone sees the URL… nobody sees the air-traffic-controller behind it.</p><p>An ALB doesn’t just “send traffic somewhere.”</p><p>It’s analyzing paths, evaluating rules, checking target health, and making split-second routing decisions, all while keeping users blissfully unaware that half of the things that are going on behind the scenes.</p><p>Today’s project was exactly that.</p><p>A hybrid ALB routing stack using Terraform — <br> <strong>/app1 → Linux + Nginx</strong><br> <strong>/app2 → Windows Server 2022 + IIS</strong></p><p>Sounds simple. And like anything simple in AWS… it worked perfectly.</p><p>…until it didn’t. And that’s where the real fun began.</p><p>This blog walks through the build, the troubleshooting, and the final lessons learned — all from the perspective of someone who’s been deep in the trenches with Terraform and EC2 user data.</p><h3>1. Designing the Architecture (aka: ALB-ers 101)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/967/1*mzLCcby-vdmQsdN8AUMBeQ.png" /></figure><p>Before writing a single line of Terraform, I stepped back and asked:</p><p><strong>“What is the ALB actually doing here?”</strong></p><p>At its core:</p><ul><li>Accept HTTP traffic on port 80</li><li>Looks at the path</li><li>Forwards to the correct target group</li><li>Keeps targets healthy using periodic checks</li><li>Returns real responses or friendly errors</li></ul><p>My path-based rules were straightforward:</p><pre>/app1* → Linux EC2 (Nginx)<br>/app2* → Windows EC2 (IIS)</pre><p>But beneath that:</p><ul><li>The ALB needs public subnets in 2 AZs</li><li>The EC2s need to live in the same VPC</li><li>Security groups must restrict inbound traffic correctly</li><li>User data must configure both OS types correctly</li><li>Health checks must match the service behavior</li></ul><p>Once the blueprint was clear, it was time for Terraform.</p><h3>2. Terraform Init &amp; Setup (tf init — the calm before the storm)</h3><p>I kicked things off with the classic workflow:</p><pre>tf init<br>tf fmt<br>tf validate<br>tf plan</pre><p>Terraform did Terraform things.<br> It complained.<br> It rejected my dreams.<br> It pointed out duplicate provider blocks I forgot I had.</p><pre>Error: Duplicate required providers configuration</pre><p>Once I removed the duplicate versions.tf provider block, the configuration became valid again — and terraform finally let me proceed.</p><h3>3. First Deployment Attempt — /app1 worked instantly, /app2… didn’t</h3><p>The ALB came up.<br> The Linux Nginx server came up.<br> /app1/ greeted me with a beautiful styled HTML page.</p><p>And then /app2/ said:</p><p><strong>504 Gateway Timeout.</strong></p><p>Classic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*k13Dc4PE6kPIDdzbmo6UGg.png" /></figure><p>Checking the Target Group for the Windows instance showed:</p><p><strong>Unhealthy — Request Timed Out</strong></p><p>This told me something critical:</p><p><strong>ALB could reach the Windows instance, but the Windows instance wasn’t answering.</strong></p><p>This narrowed it down to 3 possibilities:</p><ol><li>IIS didn’t install</li><li>User-data never executed</li><li>Firewall wasn’t allowing inbound HTTP</li></ol><p>Time to investigate.</p><h3>4. Fixing the User Data &amp; Rebuilding the Windows Instance</h3><p>I updated my Terraform to wrap the PowerShell file correctly:</p><pre>locals {<br>  windows_user_data = &lt;&lt;-EOF<br>  &lt;powershell&gt;<br>  ${file(&quot;${path.module}/scripts/windows-userdata.ps1&quot;)}<br>  &lt;/powershell&gt;<br>  EOF<br>}</pre><p>Then forced a rebuild:</p><pre>tf taint aws_instance.windows_app2<br>tf apply</pre><p>After a few minutes:</p><ul><li>IIS installed</li><li>/app2 folder created</li><li>HTML file deployed</li><li>Firewall opened</li></ul><p>But still a 502 error</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*wHltH2ScU6PLwLwWzelBAg.png" /></figure><h3>5. SSM Session Manager — Goodbye SSH &amp; RDP</h3><p>Since I couldn’t previously access either server and had to use log outputs to troubleshoot, I decided to add SSM:</p><ul><li>Created an IAM role</li><li>Attached AmazonSSMManagedInstanceCore</li><li>Added the instance profile to both EC2s</li></ul><p>Now I could SSH/RDP <strong>without SSH/RDP</strong> — directly from the browser.</p><p>This is cleaner, and also more secure.</p><h3>6. Health Check Tuning — Why Windows Needs More Patience</h3><p>Even after fixing user data, Windows took longer to initialize IIS.</p><p>My original health check:</p><pre>timeout = 5<br>interval = 30</pre><p>Windows said:<br>“Absolutely not.”</p><p>After some research I came to the following suggestion:</p><p>Increase health_check timeout and interval for aws_lb_target_group.app2 to accommodate slower Windows startup/response times.</p><p><strong>Reasoning</strong>: <strong>Windows instances take significantly longer to boot and install IIS via User Data.</strong> A 504 error often indicates the application isn’t ready or the firewall is blocking traffic.</p><p>So I adjusted:</p><pre>timeout = 10<br>interval = 60</pre><p>Now the ALB waited for IIS to finish booting and the target flipped healthy consistently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*7drKi0l59Mz2pLjO_Btaig.png" /></figure><p><strong>Lesson:</strong><br>Linux boots fast.<br>Windows boots… when it feels like it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=18684a84150a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Zero-Downtime RDS to Aurora Migration]]></title>
            <link>https://medium.com/@terminalsandcoffee/zero-downtime-rds-to-aurora-migration-77b4aa2fd242?source=rss-ca586dfe08ca------2</link>
            <guid isPermaLink="false">https://medium.com/p/77b4aa2fd242</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[database-migration]]></category>
            <category><![CDATA[mysql]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[terraform]]></category>
            <dc:creator><![CDATA[Terminals & Coffee]]></dc:creator>
            <pubDate>Tue, 25 Nov 2025 01:04:22 GMT</pubDate>
            <atom:updated>2025-11-25T01:04:22.574Z</atom:updated>
            <content:encoded><![CDATA[<p>Troubleshooting Notes from the Trenches</p><p>Greetings everyone!</p><p>In this blog I decided to do something a little different rather than the “how to build” flow and discuss some of the errors I ran into building out this project.</p><p>Today is my Saturday so I decided to I build a full end-to-end <strong>zero-downtime database migration</strong> pipeline using Terraform, AWS DMS (CDC), RDS MySQL, and Aurora MySQL.</p><p>The final setup worked flawlessly, however the road getting there was a tour through real-world AWS quirks, version mismatches, IAM edge cases, and MySQL surprises.</p><p>Here’s a clean breakdown of the issues I ran into and how I fixed each one.<br>Think of this as a mini-postmortem + lessons learned from building production-grade data migration infra.</p><p>This is the repo if you are interested:</p><p><a href="https://github.com/TerminalsandCoffee/aws-devops-portfolio/tree/main/projects/04-aurora-zero-downtime-migration">aws-devops-portfolio/projects/04-aurora-zero-downtime-migration at main · TerminalsandCoffee/aws-devops-portfolio</a></p><p>I already had the majority of the terraform code written out and saved, but I never ran the terraform flow until today. Which leads me to say — Yes I was expecting some issues but not this many LOL.</p><p>So I began by running</p><pre>terraform init</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*hye8Ynos5ZfMlctr525Vnw.png" /></figure><p>Wow no issues on the first command. I think this is going to flow smoothly.</p><p>Little did I know what lied ahead.</p><p>As anyone that’s ever worked with terraform know the next step in the flow is to run:</p><pre>terraform plan</pre><p>If you’ve been following along with any of my recent post or more recent blogs you might be able to tell that I have been trying to complete my projects as I would in a production environment. So, you will see a tag in my commands that point to a specific environment using -var-file.</p><pre>terraform plan -var-file=&quot;envs/dev.tfvars&quot;</pre><p>The -var-file= flag tells Terraform <strong>which environment’s values to load</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/749/1*AgkH_9oN4ojHEGWp1HVa1A.png" /></figure><p>Anyways, back to explaining common errors you may come across in terraform project. After running tf plan I got the following error:</p><p><strong>1. Security Group Circular Dependency</strong></p><p>Error: <strong>Cycle</strong>: aws_security_group.rds, aws_security_group.aurora</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*SQM2ptwW8fFw-pLldISoDA.png" /></figure><p>That’s a first 🤔After doing some digging I determined the following</p><p><a href="https://developer.hashicorp.com/terraform/tutorials/configuration-language/troubleshooting-workflow">Troubleshoot Terraform | Terraform | HashiCorp Developer</a></p><p><strong>The Problem</strong></p><ul><li>RDS security group referenced Aurora security group</li><li>Aurora security group referenced RDS security group</li><li>This created a cycle: RDS → Aurora → RDS</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/781/1*xUfpqhMRdA95H3jf6epMWg.png" /><figcaption>A loop of doom if you will.</figcaption></figure><p><strong>The Cause:</strong><br>RDS SG allowed inbound from Aurora SG, and Aurora SG allowed inbound from RDS SG.</p><p><strong>The Fix:</strong><br>Removed cross-references.<br>Both SGs now allow traffic <strong>only from the DMS replication instance</strong>.</p><p>Now, let’s try tf plan again. Welcome to error #2.</p><p><strong>2. Incorrect CloudWatch Log Export Names</strong></p><p><strong>Problem:</strong><br> Terraform error — slow_query is invalid.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*ZbhV9jEwt2vKqb_zJk3kag.png" /></figure><p><strong>Cause:</strong><br> MySQL log export names are strict. The correct name is slowquery (no underscore).</p><p><strong>Fix:</strong><br> Changed both RDS and Aurora log exports to use:</p><pre>slowquery</pre><p>Third times the charm right… rightttt.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*T8KP-AyeJxZ3SlfqbAARCA.png" /><figcaption>Good to Go!</figcaption></figure><p>Now to move to tf apply.</p><pre>terraform apply -var-file=&quot;envs/dev.tfvars&quot;</pre><p>If you plan to fork or clone my repo just know that should take roughtly 15–20 minutes to fully populate.</p><p>And that&#39;s without errors 🤪</p><p><strong>3. Invalid Aurora Engine Version</strong></p><p><strong>Problem:</strong><br> 8.0.mysql_aurora.3.05.2 didn’t exist in my region.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*lAKK3tps_l7qKH6nAHW2sg.png" /></figure><p><strong>Fix</strong>: Switched to a region-supported version: 8.0.mysql_aurora.3.04.0.</p><pre>8.0.mysql_aurora.3.04.0</pre><p>Easy Peasy.</p><p>Ran tf apply again, keeping an eye out…</p><p>Ok looks like things are flowing smoothly now.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*ezevwe8g-4khxLCR94PBdQ.png" /></figure><p>At this point I stopped taking screenshots and locked in to get this infra up and running.</p><h4>5. Missing DMS VPC IAM role</h4><p><strong>Problem:</strong> dms-vpc-role wasn’t configured fully.<br><strong>Fix:</strong> Created proper IAM role + attached AmazonDMSVPCManagementRole.</p><h4>6. Unsupported Aurora instance class</h4><p><strong>Problem:</strong> db.t3.small isn’t supported for Aurora MySQL 8.0.<br><strong>Fix:</strong> Switched to db.t4g.medium (supported + ARM-based).</p><h4>7. Performance Insights not supported</h4><p><strong>Problem:</strong> PI failed on a db.t3.micro RDS instance.<br><strong>Fix:</strong> Disabled PI — RDS micro instances don’t support it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*LuQxa3Tv7Kz_9eILcvYVYw.png" /></figure><p>FINALLY! I resolved the last error I thought I would run into.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*Uj5vWZtYRwXv_Kdezh5Txg.png" /></figure><p>Nice. I was able to log into a bastion host and connect with the DB host.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*0xIY_mk4sxOindIyrto0vQ.png" /></figure><p><strong>Inserted data:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*chdU8j7cOSpi82OPXeBbww.png" /></figure><p><strong>Validated data:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/1*lLq17FSiPkefp9pnPgG-yw.png" /></figure><p>Wrapping up</p><p>This project wasn’t a follow along, copy, and paste tutorial. It was a real DevOps migration build, full of the small but important details that you may or may never run into.</p><p>Lessons Learned from my first enterprise grade data migration project. <br>1<strong>. Instance class compatibility matters<br>- </strong>Aurora + MySQL 8.0 doesn’t support every T-class instance you throw at it.</p><p><strong>2. DMS has prerequisites<br>- </strong>VPC role, endpoint config, and CDC options must match the database engine.</p><p><strong>3. Security groups can easily form dependency cycles<br>- </strong>Especially in multi-tier architectures.</p><p>Now that the infra is solid, I’ll publish <strong>Part 2</strong> soon:</p><p><em>A step-by-step guide showing how to build this exact zero-downtime migration pipeline using Terraform + DMS + Aurora.</em></p><p>Thank you for following along!</p><p>I hope you learned something new or this blog helped you overcome a similar error!</p><p>Feel free to reach out to my <a href="https://www.linkedin.com/in/rgmartinez-cloud/">LinkedIn </a>to connect!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=77b4aa2fd242" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>