Resume defence
Covers every bullet: Siege (QNU Labs), Aidan, Xlancr, PeerWatch, Coding Platform, Gravitas, CookOff, CTF, Skills. JD anchor: Tech 2 is a resume deep-dive; HR/HM pushes ownership. Walmart Cybersecurity candidates report interviewers interrupting stories to demand exact numbers, dates, and outcomes — vague answers cost offers.
1. Why Resume Defence Decides the Loop
| Stage | What happens to your bullets |
|---|---|
| Tech 1 | "Walk me through one project" — they pick the one you lead with, go deep |
| Tech 2 | Resume deep-dive + scenarios; interrupt, zoom in, demand numbers |
| HR/HM | "What did you do?", "what went wrong?", "what would you do differently?" |
They score ownership (what you did), numbers (baselines + deltas), tradeoffs (rejected alternative + cost), attacker mindset (how I'd break it), failure handling (bug → root cause → fix → regression test), and calibration (honest "I haven't done X").
6-beat skeleton (every bullet):
1. Problem — one sentence. ("Tenants in one pool could resolve each other's live session.")
2. Action — what you did, tool named. ("Keyed registry by (pool, uid), fail-closed resolution, regression tests.")
3. Tradeoff — rejected alternative + cost. ("Composite key keeps ops simple; live-session migration at deploy.")
4. Attacker view — how you'd break it. ("Blank/unicode-variant uid; stale session reuse.")
5. Rebuild — one concrete improvement. ("Signed opaque tokens; authorise at use time.")
6. Stop talking. Keep 10-, 2-, and 1-minute versions of every project. STAR mapping: Problem/Action = Situation/Task/Action, metric + verdict = Result.
Discretion clause (say once): "Siege is internal QNU tooling. I'll describe architecture and technique classes; I won't name customers, keys, internal endpoints, or unrepatched bugs. The cross-tenant issue was found internally, fixed, and regression-tested." Discretion is a scored behaviour.
2. Per-Bullet Briefs
Fill every N with your real number; an honest "19 implemented and runnable" beats a fake "19 in production".
Numbers consistency across surfaces (check once, before the room): resume, repo READMEs, and spoken answers must agree, because interviewers cross-check. Name the four Siege channels the same way everywhere — the resume parenthetical lists three of four, so name all four and say why the fourth is separate. 19 techniques means 19 implemented and runnable; for production runs give the run count N and its window. Cross-tenant is 1 bug, N regression tests — and only say "no exploitation found" if the review concluded that. Label Xlancr's ~50%/70% as measured or estimated, with the window. Tie Gravitas's 3M+/₹50L+ to a named analytics source and date window. When two numbers could both be true — 200/min peak vs ~3.3/s sustained — give the conversion yourself.
2.1 Siege — AI red-team platform, 4 channels + attack-graph executor
"Four delivery channels (prompt injection, workspace planting, MCP interception proxy) and an attack-graph executor for chained multi-turn attacks."
Hook: "Real attacks chain: a planted file is read on turn three, triggers a tool call, then exfiltrates. Siege models attacks as a graph — nodes are payloads, edges are order — and runs one technique through chat, workspace, or MCP tool plane."
Two lengths for Siege (pick by signal):
30-second pitch: "Siege is an AI red-team platform I built in Go. It runs attack techniques through four delivery channels — prompt injection, workspace planting, MCP tool interception — and executes chained multi-turn attacks as a graph: a planted file read on turn three can trigger a tool call and exfiltration on turn five. It logs every step as evidence and turns findings into detection-rule drafts. I wrote the 19-technique catalog behind it, and the security review surfaced a cross-tenant session bug I fixed and regression-tested."
5-minute whiteboard: open with the chained-attack problem, draw the executor — nodes, edges, topological pass, evidence rows — then one technique end to end: payload, channel, success detector. Then the detection-rule bridge, then the cross-tenant fix in 60 seconds. Close on one limitation (the DSL is a cost; the proxy is in-process) before they find it. If they interrupt the pitch at "graph", explain nodes and edges only, then wait.
- Problem: manual single-turn testing; no chained attacks, cross-surface coverage, or reproducible evidence.
- Action: Go platform — technique catalog + engine; 4 channels; DAG of steps (channel, payload, preconditions); MCP proxy logging/rewriting tool traffic.
- Tradeoff: graph gives reproducibility + per-step attribution but needs a DSL; in-process proxy is simple, a sidecar would survive restarts and scale independently.
- Attacker view: steer the chain via a step's own output; compromise the credential-holding proxy; template/path injection via payload fields; cycles → unbounded turns.
- Rebuild: signed definitions, per-node turn/token budgets, sandboxed workspaces, zero-trust proxy (mTLS, least privilege, tamper-evident logs). Prep note: name all four channels from memory — the resume parenthetical lists three.
Why this design: a graph over a linear script list because real attacks chain across turns and surfaces — a linear runner can't express "wait for the file read, then fire the tool-call step". The cost: a DAG needs a definition format, so every technique carries schema I validate, version, and explain. The in-process MCP proxy was the same trade: rewriter and logger ship with the executor, so a run is reproducible from its own evidence — but a crash takes the interceptor with it. A sidecar survives restarts and scales independently; that's the next move.
How an interviewer attacks it:
"What stops an attack graph from looping forever?" — "Three layers. At load time I reject cycles — a DAG is a precondition, not a hope. At run time every node carries a turn and token budget and the context cancels when it's spent. And the run itself has a cap. The worst case is a bounded, evidence-filled failure, not an unbounded conversation."
"Why a detector instead of an LLM judging the transcript?" — "Because an LLM judge is a probabilistic witness and I want evidence an analyst can check. Where I can, I plant a canary and look for it in the output, a tool call, or a file read — deterministic. Where the leak is semantic, I fall back to the judge and label the verdict. I bias to precision: a false 'compliant' finding burns analyst trust in the platform."
"If you rebuilt one thing, what?" — "The proxy. Separate zero-trust service: mTLS to the executor, least privilege on the tool plane, tamper-evident logs. In-process is fine for a lab, wrong for anything long-lived. And I'd sign technique definitions — anything the executor runs should be authored, not merely present."
How an attacker breaks it (and the fix): the executor is a payload interpreter, so the definition format is the first surface — a payload field reaching a template engine or file path is injection with extra steps. Fix: parametrised rendering, path allow-lists, signed definitions. Second, the proxy holds the credentials it observes — mTLS, least privilege, tamper-evident logs. Third, the run log tells an attacker what tripped — treat evidence stores as sensitive. Cycles and budgets are handled above.
| Q | Model answer |
|---|---|
| Why Go? | Goroutine-per-branch + context cancellation; static binary deploys into restricted networks; strong HTTP stdlib. Python loses on parallelism, Rust on iteration speed. |
| Draw the executor. | Nodes (step_id, channel, payload_ref, preconditions); edges = order/dependency; topological pass runs ready nodes concurrently; persists (run_id, node_id, raw_io, verdict). Success propagates only if the detector tripped; cycles rejected at load. |
| How do you know an attack succeeded? | Deterministic detectors where possible (canary in output, secret in a tool call, file-read recorded), LLM-judge otherwise; raw I/O kept for review. Precision over recall — false "compliance" findings burn analysts. |
2.2 Siege — 19 techniques + detection-rule drafts
"Authored a catalog of 19 attack techniques … and generated detection-rule drafts from findings."
Hook: "I wrote the catalog — 19 techniques across prompt injection, credential exfiltration, and MCP tool-plane tampering — plus the bridge from finding to draft detection rule: a successful attack yields a reviewable Sigma-style draft, not a PDF nobody actions."
- Problem: findings died in reports; no coverage map; no link to the SOC pipeline.
- Action: each finding carries the observable (tool-call pattern, canary touch, anomalous sequence) → templated draft with severity, ATLAS/ATT&CK mapping, FP notes.
- Tradeoff: drafts, not auto-deploy — a canary-based rule deployed blindly false-positives on real traffic; human review costs latency but protects alert quality.
- Attacker view: rule-syntax injection via finding data; mimic legitimate automation so the rule never fires; flood canary-like traffic to get the rule muted.
- Rebuild: parametrised templating (no string concat), 3 positive + 3 negative test events per draft, rule lifecycle (draft → tested → enabled → tuned), FP feedback loop.
Why this design: the catalog is the product; the engine executes what the catalog names. I wrote the 19 techniques as data — precondition, payload, channel, success detector — so a new technique is a reviewable diff, not a release. The cost: data-driven techniques are easier to audit and harder to make expressive, so anything the format can't say becomes engine work — worth it to keep the catalog reviewable by people who don't read Go. Drafts instead of auto-deploy because whoever enables a rule owns its alert quality, and a rule deployed blind will false-positive on real traffic.
How an interviewer attacks it:
"Why 19 — how did you pick the set?" — "I started from the surfaces I could reach — chat input, workspace files, the MCP tool plane — and wrote the techniques an attacker would chain across them. 19 is what was implemented and runnable, not a target. If I'd padded to a round 20, the padding is the first thing you'd find."
"What makes a draft reviewable?" — "Four things: the observable it keys on, severity, the ATLAS/ATT&CK mapping, and false-positive notes with three positive and three negative test events. The FP notes matter most — a rule that fires on every canary touch is a rule someone mutes in week one."
"What if the SOC wants rules enabled automatically?" — "Automation at the gate, not at the write: the lifecycle draft → tested → enabled → tuned gates each step on its test events."
How an attacker breaks it (and the fix): finding data flows into rule templates, so a payload that looks like rule syntax can smuggle in a rule that never fires — parametrised templating, never concatenation. An attacker who learns the canary can touch it deliberately until someone mutes the rule — rotate canaries, rate-limit the signal. And the mimicry attack: behave like legitimate automation so the sequence never trips — which is why the rules key on sequences and egress, not single events.
| Q | Model answer |
|---|---|
| What makes a good LLM detection rule? | Key on what the defender controls: tool-call sequences, egress destinations, canary touchpoints, prompt-read attempts, rate spikes. Prompt content is a bad key — paraphrase/base64/multi-turn defeats it. |
2.3 Siege — cross-tenant session takeover (pool, uid)
Hook: "A security review found the session registry keyed by pool, not user, so a request in the same pool could attach to another tenant's live session. I keyed it (pool, uid), made UID resolution fail closed — missing/ambiguous uid denies, never falls back — and wrote takeover regression tests."
- Problem: broken access control (IDOR at the session layer) → auth bypass + cross-tenant disclosure.
- Action: composite key; fail-closed resolution; live-session migration (verify before saying); tests plant tenant A's session and assert tenant B is denied.
- Tradeoff: fail-closed causes visible outages on IdP hiccups — correct trade vs silent leak, alerted on. Opaque random tokens would eliminate the class but add a token store.
- Attacker view: blank/duplicate/unicode-colliding uid; session reaped between resolve and use; stale reuse after offboarding; wildcard pool lookup.
- Rebuild: high-entropy signed tokens bound to
(tenant, uid, expiry); authorise at use time; audit every attach; property/fuzz tests on the resolver.
Why this design: I fixed the key instead of adding another check, because a scoped key makes the bug unrepresentable — a lookup that must contain (pool, uid) can't return another user's session even if a code path forgets a guard. Fail-closed resolution is the deliberate cost: when the IdP returns nothing or something ambiguous, the user sees a denial and an alert fires, instead of a silent fallback to a shared session — visible failures over invisible leaks. The rejected alternative — opaque high-entropy tokens — kills the whole class but adds a token store and rotation story; I named it as the next step.
How an interviewer attacks it:
"What happens to sessions already live when you deploy the fix?" — "The migration path is the part I verified before claiming anything. Old sessions either re-key cleanly or are reaped and force re-login. If a live session can't be re-keyed safely, I'd rather terminate it than leave it ambiguous. Which happened is a deploy fact — I checked it rather than smoothing it."
"Fail-closed is fine until an IdP outage denies a whole tenant — still the right trade?" — "Yes: a denial is visible, a leak is not. The mitigation is operational — alert on denial rate so an IdP hiccup pages someone. If availability ever forced a fallback, it would be an explicit, audited break-glass path with reduced scope, never a silent 'blank uid means shared pool'."
"Why not just add an authorisation check where sessions attach?" — "Because checks live on code paths and code paths multiply. I wrote the tests that assert the check, but the structural fix is the key: every path that resolves a session now gets a per-user session or an error. Scoped key plus takeover regression tests — plant A's session, assert B is denied — together are what I'd want in anyone's PR."
How an attacker breaks it (and the fix): against the old key the attack is a plain IDOR — request the same pool, name a victim, attach. After the fix the resolver is the target: blank, duplicate, or unicode-colliding uids, a wildcarded pool lookup, or a session reaped between resolve and use. The resolver matrix tests those and fails closed on all. The deeper fix is binding: high-entropy signed tokens carrying (tenant, uid, expiry), authorised at use time, every attach audited — so even a resolver bug becomes visible.
| Q | Model answer |
|---|---|
| Explain it like a PM. | "Sessions were per worker pool, but the lookup asked 'which pool?', not 'which user?'. Two customers in a pool could attach to the same session — one seeing another's data. Only a logged-in customer could trigger it; that's the boundary that matters." |
| How did you find and prove it? | Traced every registry read/write asking "does the key contain the principal?" It didn't. Wrote a failing A→B attach test first, then fixed; resolver matrix (missing, blank, duplicate, unicode) fails closed. |
| IDOR analogy? | Same root cause: an object reference valid for one principal but not scoped to the requester. Fix the same way — authorise on object + principal, deny by default. |
| Did you disclose it? | Internally, via the review process; fixed and regression-tested. (Say calmly; teams listen for over-dramatisation. Only say "no data left / no exploitation found" if that is what the review actually concluded — otherwise: "I know it was fixed and regression-tested; I can't speak to the incident analysis.") |
2.4 Aidan — AI retrieval observability
Hook: "A bad answer looked like a good answer — failures were invisible. I added request-scoped structured logging, an admin failed-runs view showing the failing stage, and automatic retention. Diagnosis went from 'grep timestamps' to 'open failed-runs, replay the stage'."
- Problem: silent retrieval/ranking failures; no per-request trace; unbounded log accumulation.
- Action: request-id logging (stage, latency, retrieval count); failed-run admin views; windowed auto-retention.
- Tradeoff: verbosity vs PII — metadata/hashes by default, raw prompts/documents only in short-TTL debug mode. Retention long enough to debug vs compliance cost.
- Attacker view: log injection via newlines in retrieved content; PII/secrets in prompts; admin-view IDOR; retention job failing silently; huge payload logging as resource abuse.
- Rebuild: redaction at the logging boundary, field allow-list, size caps, per-tenant RBAC on admin views, monitored deletion job, break-glass capture with audit.
Why this design: observability at the request boundary because that's the only place all stages meet — per-stage logs with no request id is grep archaeology. The cost: metadata and hashes by default means some investigations end in "enable debug and reproduce", and I chose that over storing raw prompts. Windowed retention for the same reason: a log store is a data store, and "keep it forever" is how a debugging aid becomes a breach.
How an interviewer attacks it: "First thing you check for a wrong answer?" — "Retrieved chunk ids and their scores. Right chunk retrieved but ranked below cutoff is a retrieval failure; right chunk in the prompt and ignored is a generation failure. That fork is why I log stage timings per request id." "What would make you log raw prompts?" — "A break-glass flag: short TTL, per-tenant, audited. Never a default."
How an attacker breaks it (and the fix): retrieved content flows into logs, so newlines and control characters forge log lines — redact at the logging boundary and cap sizes. The failed-runs admin view is itself an object lookup, so it inherits §3.2: per-tenant RBAC, authorised at use time. And the retention job is a deletion promise — monitor it, or "30-day retention" is a claim, not a control.
| Q | Model answer |
|---|---|
| What exactly do you log for a RAG request? | Request id; hashed tenant/user; query hash + length; retrieved chunk ids/scores (not text); stage timings; model + version; guardrail verdict; error class. Raw text only in opt-in debug. |
| Debug a wrong answer in production? | From the request id: which chunks were retrieved and their scores → right chunk ranked low (retrieval) vs ranked high but unused (generation). That fork is the point of stage-level logging. |
2.5 Xlancr — multi-layer Redis/FastAPI cache (~50%, 70% coverage)
Hook: "The API recomputed expensive, mostly-identical responses. I added a layered cache — in-process for hot keys, Redis for shared — with explicit key construction and invalidation on write. Cost and latency dropped roughly half; coverage rose to 70%, gated in CI."
- Problem: redundant expensive computation; no cache; no test gate.
- Action: two-layer cache-aside (local TTL → Redis), key = endpoint + normalised params + user/tenant + version tag; invalidation on writes + bounded TTL; CI coverage gate.
- Tradeoff: cache-aside tolerates Redis downtime (fall back to compute) but has a stale window; write-through is fresher but couples writes to Redis. A coverage gate can be gamed — a floor, not quality.
- Attacker view: unkeyed input (header/cookie) shapes a response → cache poisoning; missing tenant in key → cross-user leak (same class as 2.3); stampede on expiry; caching auth decisions.
- Rebuild: explicit key builder with allow-listed inputs + a test that two users never read each other's entry; single-flight locks; negative caching; hit/eviction metrics; never cache auth decisions.
Why this design: cache-aside over write-through because the read path had to survive Redis being down — falling back to compute degrades latency; write-through failure breaks writes. The cost is a stale window, bounded by TTL and version-tag invalidation. I accepted "correct with a small stale window" over perfect freshness at database load. The version tag makes invalidation cheap: a write bumps it and old entries age out.
How an interviewer attacks it: "What's in the key?" — "Endpoint, normalised params, user/tenant, version tag. If a header or cookie shapes the response and isn't in the key, that's poisoning; if the tenant isn't in the key, that's a cross-user leak — same bug class as the Siege session key, and I have a test that two users never share an entry." "Hot key expires — what happens?" — "Stampede: everyone recomputes at once. Single-flight means one goroutine recomputes and the rest wait; negative caching handles repeated misses on data that doesn't exist."
How an attacker breaks it (and the fix): unkeyed input shapes a cached response (poisoning), a missing tenant in the key leaks across users (the §3.2 class), deliberate misses force expensive recomputation (compute DoS), and caching auth decisions turns a stale entry into an access grant. Fix: allow-listed key builder, the two-users-never-share test in CI, single-flight, negative caching, hit/eviction metrics — and auth decisions stay out of the cache.
| Q | Model answer |
|---|---|
| ~50% — measured how? | (Be exact.) "Same workload before/after: p50/p95 latency and per-1k-request upstream cost over two weeks." If it was an estimate, say so — "roughly half from dashboard deltas" — and how you'd measure properly. |
| Invalidation? | Writes bump a version tag for the affected entity and clear that key family in both layers; a bounded TTL is the safety net. "Correct with a small stale window" over perfect freshness at DB load. |
2.6 PeerWatch — Go P2P full-mesh TCP, 10 message types, 512KB SHA-256 chunks
Hook: "Video isn't re-streamed — peers sync playback and fetch media chunks from each other over a full-mesh TCP topology. I hand-wrote the wire protocol: 10 message types, length-prefixed framing, 512KB chunks each SHA-256 verified."
- Problem: centralised streaming is bandwidth-heavy; wanted peer media sharing + synchronised playback.
- Action: Go TCP listeners per peer, full mesh, type + length framing, sync messages, hash-verified chunk transfer,
mpvIPC playback. - Tradeoff: TCP gives ordering/reliability but head-of-line blocking (not NAT-friendliness — UDP hole punching is more reliable, ~82% vs ~64% of NATs, RFC 5128 / MIT PDOS); full mesh has no bottleneck peer but O(n²) connections (small rooms only); custom protocol is low-overhead but framing bugs are on you.
- Attacker view: plaintext, unauthenticated — MITM reads/modifies; SHA-256 catches corruption, but a malicious peer can serve valid chunks of the wrong content; spoofed identity; crafted length → memory exhaustion; slow-chunk DoS.
- Rebuild: TLS/Noise with per-peer identity keys; signed manifest binding hashes to a room/version; authenticated joins; read deadlines, max-frame validation, flow control; hybrid mesh + relay fallback.
Why this design: full mesh because a watch-together room has no bottleneck peer and every hop is one connection away — the cost is O(n²) connections, fine at room scale, wrong at broadcast scale. Hand-written framing because 10 message types and a length prefix are all the protocol needs — the cost is every framing bug is mine, so max-frame validation and read deadlines are part of the protocol. TCP for ordering on sync, accepting head-of-line blocking as the price.
How an interviewer attacks it: "What does the hash actually buy you?" — "Corruption detection in transit and on disk. Not authenticity: if the peer controls the hash list, valid chunks of the wrong video pass. Authenticity needs a signed manifest binding hashes to a room and version." "Why not QUIC?" — "Ordered sync mattered more than minimal latency and I wanted the standard library. QUIC's streams and built-in TLS are the right modern answer; for an internet-facing rebuild I'd take the dependency."
How an attacker breaks it (and the fix): the wire is plaintext and unauthenticated — a MITM reads and modifies playback sync; a malicious peer serves valid-but-wrong chunks; a crafted length field exhausts memory. Fix: TLS or Noise with per-peer identity keys, a signed manifest, authenticated joins, frame limits with read deadlines. | Q | Model answer | |---|---| | Why TCP, not QUIC/UDP? | Needed ordered sync and reliability more than minimal latency. Not for NAT — traversal needs hole punching or a relay regardless. QUIC (built-in TLS, streams, no head-of-line blocking) is the modern answer, but a heavier dependency. | | NAT traversal? | Honestly: it dials directly and works on LANs/known networks. Consumer NATs need STUN + hole punching with TURN relay fallback; today it fails visibly when peers aren't reachable. | | What does SHA-256 NOT catch? | Malicious replacement — if the peer also controls the hash list, valid-but-wrong content passes. Integrity needs a signed/anchored manifest from a trusted source. |
2.7 Coding Platform — Judge0 + Asynq + Gin + sqlc, 600+ users
Hook: "Gin API, sqlc queries, Redis-backed Asynq queue, Judge0 for sandboxed execution. The API only enqueues and returns a token; workers execute and persist verdicts — a slow or malicious submission never blocks an HTTP worker. A live contest ran on it with 600+ users."
Honesty line: "Judge0 is the sandbox I integrated, not one I wrote — my ownership is the pipeline: queueing, status, idempotency, limits, API." Claiming you built it then fumbling isolate/seccomp destroys credibility. Verify before the room: enable_network=false and allow_enable_network=false in judge0.conf (CE defaults are true — GHSA-q7vg-26pg-v5hr is why I checked).
- Problem: untrusted code execution at contest scale; sync execution exhausts web workers; duplicate/lost jobs give wrong verdicts.
- Action: submit → validate → enqueue (Asynq) → worker → Judge0 (timeout) → persist verdict → leaderboard; per-user rate limit; job keyed to
(user, problem, submission). - Tradeoff: async (Asynq over
SKIP LOCKED/Kafka: retries + visibility, small ops surface) isolates blast radius and absorbs spikes but adds polling latency and a backlog failure mode; Judge0 gives well-tested isolate-based sandboxing when configured, but is a dependency with less limit control. - Attacker view: fork/thread bomb; miner; SSRF to cloud metadata (169.254.169.254) or internal services; FS escape; submission flood starving the queue; zip-bomb tests; forged verdict callback.
- Rebuild: verify + document controls (no network, read-only root, non-root, dropped caps, seccomp, cgroup PID/mem caps, wall + CPU timeouts); idempotent processing with state guards; queue-depth backpressure; verdict signing; CI suite of hostile submissions.
Why this design: the API only enqueues because HTTP workers should never wait on untrusted code — a slow submission occupies a queue slot, not a request thread. Asynq over Postgres SKIP LOCKED because retries and visibility come free and Redis was already running; over Kafka because a contest doesn't need a broker. The cost: polling latency, a backlog failure mode if workers die, and Judge0 as a dependency whose sandbox config I must verify.
How an interviewer attacks it: "What did you actually build?" — "The pipeline: queueing, status, idempotency, limits, API. Judge0 is the sandbox I integrated and configured — I didn't write isolate, and I won't claim I did." "One config you'd check first?" — "Network. The CE config can let a submission enable its own network, so I deny that and keep a drift check — submission-as-SSRF is the highest-value attack on a judge."
How an attacker breaks it (and the fix): fork bombs and miners (PID/CPU/mem caps, wall timeout), SSRF to cloud metadata or internal services (no egress), filesystem escape (read-only root, non-root, dropped caps), queue starvation (rate limits plus backpressure), forged verdict callbacks (job keyed to (user, problem, submission) plus state guards). Validated by a CI suite of hostile submissions asserting verdicts.
| Q | Model answer |
|---|---|
| Why Asynq? | Redis-backed Go queue with retries, scheduling, and visibility (pending/active/failed), small ops surface; Redis was already running. Alternatives: Postgres SKIP LOCKED (simpler, polling latency) or Kafka (overkill). |
| Fork bomb prevention? | PID cap, CPU time, memory cap, no network (deny allow_enable_network in config + drift check), non-root, dropped caps inside Judge0. I validated with hostile submissions (infinite loop, fork loop, socket connect) and asserted verdicts — not by reading docs. |
| A submission is stuck "Pending" — debug? | Trace the id: DB state → Asynq task state → worker logs → Judge0 health. Classify: never-picked (worker down/backlog), picked-but-hung (timeout/socket), completed-no-persist. Add oldest-task-age + queue-depth dashboard. |
2.8 Gravitas — seat-hold race, transaction leak, 3M+ visits / ₹50L+ / dashboards
"Eliminated booking race conditions via a Redis Sorted Sets seat-hold … Resolved a critical transaction leak on all code paths, preventing connection pool exhaustion; admin dashboards reduced support tickets by 95%."
Hook (race): "Two people could pass the 'is this seat free?' check and both pay — check-then-act. I made the hold atomic: seats are ZSET members scored by hold-expiry, so acquiring is one atomic op and expiry is a range query. Losers get 'held by someone else'."
Hook (leak): "Error paths skipped transaction cleanup, leaking connections until the pool was exhausted. I made every path release — defer rollback + explicit commit — added pool-saturation monitoring, and validated by simulating failures. Dashboards cut tickets 95%."
- Problem: TOCTOU oversell; leaked transactions → pool exhaustion; support blind.
- Action: ZSET hold (
ZADDscore = expiry epoch, atomic acquire via Lua/NX,ZRANGEBYSCOREavailability,ZREMrelease, TTL sweeper); confirm → durable Postgres row;defer tx.Rollback()on every path; admin dashboards. - Tradeoff: Redis gives speed and native TTL but creates dual-write consistency with the system of record (reconcile on confirm, expire on failure). Longer holds improve UX but cut inventory turnover.
- Attacker view: hold hoarding; bypassing the hold at payment (payment must re-validate server-side, never trust the client); replay of hold/release tokens; clock skew breaking expiry; keys not namespaced per show → collisions; dashboard IDOR. Leak view: deliberately triggering error paths (bad payloads, cancel-then-retry) to drain the pool = application-layer DoS — pool-saturation monitoring is a security control.
- Rebuild: per-user hold caps; payment needs a signed single-use hold token and re-checks atomically; keys
show:{id}:seat:{id}; RBAC + audit on dashboards; N-goroutine test hammering one seat asserting exactly one winner.
Why this design: the ZSET hold collapses check-and-hold into one atomic operation because the score is the expiry — "what's still held" is a range query and expiry is free. The cost is dual-write consistency: Redis holds inventory state briefly while Postgres is the system of record, so confirm reconciles and failure expires. Longer holds improve UX but stall turnover, so hold TTL is a business knob. On the leak, the fix was structural: every path releases — defer rollback plus explicit commit — because "remember to clean up" is not a control.
How an interviewer attacks it: "Where can it still oversell?" — "At payment, if that path trusts the client or skips the atomic re-check. Payment re-validates against the hold with a single-use signed token and one atomic op." "How did the leak ship?" — "Error paths didn't own cleanup — happy path released, failure path didn't. I found it by simulating failures and watching the pool drain; the saturation monitor I added is also the DoS detector."
How an attacker breaks it (and the fix): hold hoarding (per-user caps), replayed hold/release tokens (single-use signed), clock skew breaking expiry (server-side clock only), key collisions across shows (namespaced show:{id}:seat:{id}), dashboard IDOR (RBAC + audit). And the application-layer DoS: triggering error paths to drain the pool — fixed by deferred rollback, watched by the saturation alert.
| Q | Model answer |
|---|---|
| Why a Sorted Set? | The score is the expiry, so "what's still held" is one ZRANGEBYSCORE and expiry is cheap. A plain SET loses ordered range reads; a hash loses ordering entirely. |
| Is the ZSET trick atomic? | Single commands are; the danger is check-then-ZADD in app code. Acquisition is one Lua script (membership + add) or ZADD NX semantics, so it's atomic server-side. |
| Prove the race is gone. | Concurrency test: N goroutines attempt one seat → exactly 1 success, N−1 clean rejections, repeated. Plus a production invariant: oversell count is a monitored metric that must stay zero. |
| Explain it like a PM. | "Two people paid for one seat because the system checked, then acted — I made checking and holding one atomic step." |
| 3M / ₹50L / zero downtime — source? | Analytics + payment-record totals; "zero downtime" = no payment-critical outage/error burn above threshold during the run window. (Have the exact window and source ready.) |
2.9 CookOff 9.0 — 5-member team, 15k evals, 200/min, <10s
Hook: "A cooking-themed event portal; I led the 5-person backend team and built the submission/judging pipeline. 15,000+ evaluations, peaking around 200 submissions a minute, average verdicts under 10 seconds, no downtime."
- Problem: short high-spike event; fast fair judging; auth + anti-abuse.
- Action: service split (auth, submissions, judging queue, leaderboard, notifications); queue-based judging; backpressure; per-user rate limits.
- Tradeoff: simple per-submission limits over aggressive anti-collusion detection (timeboxed event; false accusations worse than the risk); workers sized to peak (~3.3/s sustained) idling between events.
- Attacker view: submission flood starves other teams; duplicate submissions game ranking; IDOR on submissions; shared team credentials; forged leaderboard writes.
- Rebuild: fairness quotas + reserved capacity, content-hash dedupe, server-only leaderboard writes, load test at 3× observed peak, incident runbook (queue drain, worker restart, surge promotion).
Why this design: the event is short and spiky, so I sized workers to peak (~3.3/s sustained) with headroom and let the queue absorb bursts — over-provisioning a two-day event is cheap insurance. The cost: simple per-submission limits instead of anti-collusion detection, because the event was timeboxed and a false accusation is worse than the residual risk.
How an interviewer attacks it: "What unfairness did you leave unsolved?" — "Collusion inside a team — shared solutions. Quotas and dedupe bound the blast radius; for a timeboxed event I accepted the rest." "Hardest bug?" — Pick a real one from the table below.
How an attacker breaks it (and the fix): a submission flood starving other teams (fairness quotas, reserved capacity), duplicate submissions gaming ranking (content-hash dedupe), IDOR on submissions, forged leaderboard writes (server-only writes). Prove it with a load test at 3× observed peak and keep the runbook ready.
| Q | Model answer |
|---|---|
| 200/min — how sized? | 200/min ≈ 3.3/s sustained, bursty; workers = peak ÷ per-worker rate with ~2× headroom, queue absorbs spikes; validated in a dress rehearsal rather than guessed. |
| Hardest bug? | (Pick a real one.) E.g., duplicate verdicts from at-least-once delivery corrupting the leaderboard → idempotent upsert keyed by submission id + state guard; or worker crash leaving submissions "judging" forever → reaper requeues past timeout. |
2.10 CTF — 2nd place, Ghidra/GDB
Hook: "I placed 2nd in the CTF, solving reverse-engineering and binary-analysis challenges with Ghidra and GDB. Static first — Ghidra decompile, xrefs to interesting calls — then GDB: break on comparisons, dump the flag from memory."
- Problem: obfuscated binaries, unknown logic, memory-corruption challenges.
- Action: static (strings, imports, decompile, rename, xref to
strcmp/crypto); dynamic (breakpoints, registers/stack, memory dump). Concept readiness only — say only if true: pwn offset via cyclic pattern, checksec, ROP/ret2win. - Tradeoff: static is safe but slow on packed/anti-analysis binaries; dynamic is faster but can be fooled and needs an isolated VM. Timebox: 15 min static, then rotate.
- Attacker view: these tools ARE incident-response malware triage — same hypothesis-and-verify loop on an unknown binary.
- Rebuild: scripted tooling (Ghidra scripts, pwntools templates), a writeup per challenge, snapshot/VM workflow.
Why this workflow: static first because reading decompiled code is safe and cheap; move to GDB when the question becomes "what is this value at runtime". The cost: static crawls on packed binaries and dynamic can be fooled by anti-debug checks — so it's a rotation (15 minutes static, then move) inside a snapshot VM, and I say the packed-binary case is at my boundary.
How an interviewer attacks it: "Walk one challenge end to end." — Prepare this story (template in the table above). "What if it's packed?" — "Identify the packer from headers and imports, run it in a VM, dump at the original entry point — honestly, that's at my boundary; my strength is the plain stripped-binary case, and I'd tell you that before you found it."
How an attacker breaks it (and the fix): this section is the attacker view — triage an unknown binary, hypothesise from strings/imports/behaviour, verify in a debugger. The rebuild is workflow: scripted tooling so analysis is repeatable, a writeup per challenge so the method compounds, snapshot VMs so hostile samples stay contained.
| Q | Model answer |
|---|---|
| Walk one challenge end to end. | Prepare this story. Template: "A stripped ELF compared input byte-by-byte to an embedded constant. In GDB I broke on the compare, read the expected byte from a register each iteration, recovered the flag, then verified it." |
| Buffer overflow basics? (concept readiness — answer only if you have actually done pwn) | Offset to return address via cyclic pattern → check mitigations (NX, PIE, canary, ASLR) → redirect: ret2win, else ROP, or leak-and-return to defeat ASLR/PIE. |
2.11 Skills table — depth line + boundary per tool
| Skill | Depth line | Boundary (say it) |
|---|---|---|
| Go | Goroutines/channels/context, net/http, interfaces, sync, testing |
Generics/reflect shallow; scheduler internals textbook-level |
| Rust | Ownership/borrowing, Result/?, traits, Cargo, async basics |
Production Tokio at scale; complex lifetime generics need docs |
| TypeScript | Types/generics, strict mode, Node backends | Advanced mapped/conditional types |
| SQL | Joins, indexes, plans, ACID, sqlc/Prisma/Drizzle | Complex window functions — would look up |
| Flask / Django / NestJS | Blueprints + factory / ORM + migrations + auth / modules + DI + guards | Django async, NestJS GraphQL federation |
| Postgres / Mongo / Redis / DuckDB | MVCC, EXPLAIN, pooling, upserts / aggregation, indexes / ZSET, TTL, Lua, caching / columnar OLAP, Parquet | Sharding, cluster mode, replication internals — conceptual |
| Docker / Linux / Actions | Multi-stage, limits, non-root / permissions, systemd, logs, ss/lsof, hardening / matrix, caching, secrets, gates |
Kubernetes is a real gap; eBPF; self-hosted runners |
| Burp Suite | Proxy/Repeater/Intruder, login/search flow tests | Scanner-only use; advanced extensions |
| Wireshark | Display filters, follow TCP stream, cleartext-cred/DNS-tunnel spot | Large-scale capture automation |
| Autopsy / ProDiscover | File carving, deleted recovery, timeline basics | Deep filesystem-internals forensics |
| Malware analysis | Static (strings, hashes, VT, PE headers) vs dynamic (sandbox) | Packed-binary unpacking, kernel-level — would look up |
Rule: at a boundary: "I've used X but not that deep — nearest is Y; I'd learn it via Z." Calibration beats bluffing.
2.12 Links live
Pin the 3 repos you'll defend, make READMEs demo-ready, and keep sophic.dev open in a tab. Checklist in §4.
What "defend a repo" means: any file they open should answer "why is it written this way?" when pointed at. If a README states a number, it must match the numbers-consistency note above. Archive old repos rather than hiding them — an honest README with a limitations section reads better than a deleted one.
3. Cross-Cutting Themes (askable against any bullet)
§2.2 is the Tech-2 differentiator: "a good LLM rule keys on defender-controlled signals (tool-call sequences, egress, canary touches) — never prompt text."
3.1 Sandbox escapes
"Controls layer: filesystem (read-only root, per-run tmpfs, no docker.sock), process (non-root, dropped caps, PID cap, seccomp), resources (cgroup caps, wall + CPU timeouts), network (no egress or default-deny). I test host mounting, runtime-socket abuse, kernel bugs, and submission-as-SSRF-pivot to cloud metadata or internal services."
| Attack | Control | Test |
|---|---|---|
| Fork/thread bomb; miner | PID + CPU/mem caps; wall timeout | Fork/busy loop → EAGAIN/killed |
| Metadata SSRF | No egress; IMDSv2; blocked routes | connect(169.254.169.254) denied |
| Host FS read; runtime socket | Read-only root, tmpfs, no docker.sock, drop caps |
/etc/shadow fails; socket path absent |
| Queue starvation | Rate limits, backpressure | Flood → 429/delay, workers alive |
Submitter enables own network (Judge0 allow_enable_network defaults true) |
Deny in judge0.conf + config drift check |
POST with enable_network=true → still no egress |
3.2 IDOR / keying
"Any lookup taking an identifier must be scoped to the principal and authorised at use time, not just at lookup. The cross-tenant fix and cache-key design are the same lesson: the key must contain the tenant/user." Test matrix: missing key part, null/empty, type confusion, unicode normalisation, wildcard, stale entry, check-use race, cross-tenant enumeration.
3.3 Race conditions
Say TOCTOU and atomic. Find the check-then-act → collapse into one atomic op (Redis Lua/ZADD NX, Postgres ON CONFLICT, SELECT … FOR UPDATE) → prove with a concurrency test asserting exactly one winner. Seat-hold and session-attach are the same pattern.
3.4 Secrets
Never in code/git (pre-commit hooks + scanning; if leaked, rotate first, purge second); scoped per environment, least privilege, short TTLs; redacted in logs/errors; CI secrets masked and unavailable to fork PRs; never open .env or a password manager on screen-share.
Why rotate first: a leaked key's blast radius only grows in a history you can't fully purge — rewriting history is cleanup, rotation is the fix. Hooks and scanners detect, not prevent; assume anything ever committed is public.
3.5 PII in logs
"A log store is a data store: minimise by default (hashes/ids, not raw text), cap and truncate, redact at the logging boundary, enforce retention with a monitored deletion job, and RBAC readers. Audit trails are append-only." At Walmart scale — customer, payment, associate data — defaults must be conservative; LLM/agent logs are a new leak surface.
4. Live-Demo Checklist (screen-share)
| # | Item |
|---|---|
| 1 | Pin 3 repos you'll defend; archive the rest; each README has architecture, run steps, tests, limitations, 2-min demo path |
| 2 | Clean-clone test in a fresh dir; follow your own README literally; fix every gap |
| 3 | Secret scan: git log -p | grep -iE 'key\|token\|secret\|password'; always run gitleaks/trufflehog even if grep is clean (grep misses base64/hex) |
| 4 | Fallbacks: 90-sec recording (tested end-to-end: audio + share permissions), screenshots, architecture PDF, local build |
| 5 | Rehearse 10/5/2-min scripts out loud, timed; run an interruption drill ("why that choice?") and recover without restarting |
| 6 | Numbers sheet: every resume metric + source (dashboard/analytics/PR); attacker-view sheet: one line per project |
| 7 | Screen hygiene: notifications off, personal tabs closed, no .env/password manager/autocomplete, readable zoom |
| 8 | Join 5 min early; test A/V + share; phone fallback; pre-open repo file, terminal dir, running services |
| 9 | Show code only to prove a decision (a diff, a test, 20 lines); close with one honest limitation before they find it |
5. Revision Table
| Bullet | Hook (first clause) | Number | Hardest follow-up | Attacker view | Rebuild |
|---|---|---|---|---|---|
| Siege — channels/graph | "Single-prompt testing is obsolete; attacks chain" | 4 channels, N runs | Draw the executor | Steer the chain; own the proxy | Signed definitions + budgets |
| Siege — techniques/rules | "Finding → draft rule, not a PDF" | 19 techniques, N drafts | What makes a good rule? | Rule-syntax injection | Templating + FP loop |
| Siege — cross-tenant | "Registry keyed by pool, not user" | 1 bug, N tests, 0 exploitation | Explain like a PM | Blank/unicode uid; stale reuse | Signed tokens + authz at use |
| Aidan | "A bad answer looked like a good answer" | Time-to-diagnose down (qualitative unless you instrument MTTR) | PII in logs? | Log injection; admin IDOR | Redaction + RBAC + monitored deletion |
| Xlancr | "Recomputing identical expensive responses" | ~50%, 70% | "~50% measured how?" | Unkeyed input → poisoning | Key builder + single-flight |
| PeerWatch | "Video served P2P, not re-streamed" | 10 types, 512KB | NAT traversal? | Plaintext MITM; wrong-but-valid chunks | TLS/Noise + signed manifest + relay |
| Coding platform | "API enqueues; HTTP never blocks" | 600+ users | Fork bomb? | Queue starvation; SSRF; forged verdict | Hostile-submission CI suite |
| Gravitas — hold | "Check-then-act oversell → one atomic hold" | 3M+, ₹50L+, zero downtime | Why ZSET? | Hoarding; bypass at payment | Caps + server re-check + race test |
| Gravitas — leak | "Error paths drained the pool" | Leaks on error paths → 0, tickets −95% | How did it ship? | Error-path flood = pool-exhaustion DoS | defer + CI lint + saturation alert |
| CookOff | "High-spike judging, 5-person team" | 15k, 200/min, <10s | p95? Hardest bug? | Duplicates; IDOR; forged writes | Quotas + dedupe + server-only writes |
| CTF | "Static then GDB; RE is triage in miniature" | 2nd place, N challenges | Walk one challenge | (this IS the attacker view) | Scripted tooling + writeups |
| Skills | Depth line + boundary per tool | — | Rate yourself with an example | — | Name the nearest transferable skill |
Source rule: every number in this table needs a source tag in prep (dashboard, PR, analytics export, or estimate — never state an estimate as measured). Dates (resume): QNU 09/2026–Present; Aidan 08–09/2026; Xlancr 09/2024–02/2025; PeerWatch 08/2026; Coding Platform 09/2024; Gravitas 09/2025; CookOff 09/2024.
6. Mistakes That Kill This Round
| Mistake | Sounds like | Fix |
|---|---|---|
| Vagueness | "We improved performance" | Baseline, delta, method, window |
| No numbers | "A lot of users", "much faster" | One number per bullet + source; label estimates |
| Bluffing / overclaiming | "I built the Judge0 sandbox" | "I integrated and verified it; I built the pipeline"; "haven't done X, nearest is Y" |
| Badmouthing the team | "Other devs didn't test anything" | "The failure path wasn't owned, so I added the check" |
| Blame-shifting | "My manager rushed it" | Own your part; describe the constraint like an engineer |
| Monologue | 5 uninterrupted minutes | 20-sec hook, stop, let them steer |
| Confidentiality leak | Customer names, keys, unrepatched bugs | Discretion clause; architecture/technique classes only |
| No attacker view / no limitation | Happy-path only; "basically production-ready" | One line per project: "how I'd break it"; volunteer one gap first |
| Demo improvising | Searching folders on screen-share | Pre-open artifacts + fallback recording + timed rehearsal |
| Arguing | Defending against pushback | "Fair point — the tradeoff I accepted was X; I'd change Y" |
Final drill: for three bullets — cross-tenant fix, seat-hold race, caching — deliver the 6-beat answer in ≤90 seconds, then have someone interrupt with a random §2 follow-up. Three down, twelve mastered.