interview prep

Threat modeling interview for security engineers, a STRIDE example

The prompt fits on one line: “Here’s how we ship code to production. Threat model it.” The next hour tests whether you can find the two threats that matter in a system you’ve never seen.

Amazon’s security engineer interview prep page lists threat modeling among the technical competencies in a loop of five 60-minute interviews, and says each interviewer will typically ask two or three behavioral questions tied to its Leadership Principles. The whole loop is covered in why the security engineer loop rewards judgment over trivia, and the rest of the section in the security engineer interview guides.

A GitHub Actions deploy pipeline for a payments API

Deploy pipelines make good practice systems: they hold the keys to production and run code from people you don’t control. Acme is a fintech with about 200 engineers on the GitHub Team plan. Its payments API lives in a private repo, acme/payments-api. Every engineer can push branches, and a merge to main deploys to production with no further human step. ci.yml runs on every push and pull request: unit tests, then integration tests on a self-hosted runner in the staging VPC, near the staging database. deploy.yml runs on pushes to main: build the container, push it to Amazon ECR, helm upgrade the production EKS cluster. The job trades a GitHub OIDC token for the deploy-prod IAM role through sts:AssumeRoleWithWebIdentity, so no AWS keys are stored. Two long-lived repo secrets remain: NPM_TOKEN, which publishes the internal SDK, and DATADOG_API_KEY. Actions are referenced by tag, like @v4.

The whiteboard version, boundaries as ==== lines:

 Engineer laptop
      | DF1  git push, open PR
 ============================== TB1  person -> GitHub identity
      v
 GitHub repo acme/payments-api      (merge to main = deploy)
      | DF2  push or PR on ANY ref runs that ref's workflow file
 ============================== TB2  any ref -> production access
      v
 GitHub-hosted runner  <-- DF3 --  tagged actions, npm packages
      |             ================ TB3  our job -> others' code
      | DF4  OIDC token -> sts:AssumeRoleWithWebIdentity
 ============================== TB4  GitHub -> AWS account
      v
 IAM role deploy-prod
      | DF5  docker push             | DF6  helm upgrade
      v                              v
 ECR payments-api  --- DF7 pull ---> EKS production cluster
                   ================= TB6  registry, deployer -> prod

 PR integration tests run on a persistent self-hosted
 runner in the staging VPC.
                   ================= TB5  untrusted ref -> staging

Six trust boundaries, named before any threat

Threats cluster where trust boundaries are crossed, so name the boundaries first. TB1 runs between a person and a GitHub identity: GitHub believes whoever holds a valid session or token.

TB2 runs between any ref and production access. Candidates tend to draw it between feature branches and main, as if branch protection guarded production. It doesn’t. For push and pull request events, GitHub runs the workflow file from the triggering ref, so a branch’s author decides which secrets its workflow reads and whether it requests an OIDC token. Most CI systems work this way: pipeline config lives in the ref, so whoever can push a ref decides what CI runs and with which credentials.

TB3 separates Acme’s job from code it didn’t write. At TB4, the deploy-prod trust policy is the entire boundary between GitHub and AWS. TB5 is the self-hosted runner’s reach into staging. TB6 decides which images the cluster runs and what the deployer may change.

STRIDE threat model of a GitHub Actions to AWS deploy pipeline

STRIDE came out of Microsoft, where Loren Kohnfelder and Praerit Garg wrote it up in 1999, and Microsoft’s Threat Modeling Tool still sorts threats into its six categories: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service and Elevation of privilege.

ID Pipeline element or flow (GitHub Actions to AWS EKS) STRIDE category Concrete threat Impact Likelihood Mitigation
T1 TB1, DF1: engineer pushes to GitHub Spoofing A phished GitHub session or leaked classic token pushes branches as a real engineer High: the way into T2 Medium: 200 engineers, tokens in dotfiles Org-required secure 2FA, ideally security keys or passkeys; fine-grained expiring tokens
T2 TB2, DF2: any branch runs its own workflow Elevation of privilege A branch edits its workflow to read repo secrets and request an OIDC token; review on main never sees it Critical: with T7, production AWS Medium: needs only push access Production credentials reachable only from an Environment limited to main
T3 TB2, DF2: merge to main Tampering Unreviewed code reaches main through an admin bypass, a push after approval, or a bot approval Critical: a merge is a deploy Medium: bypass lists grow, and stale-approval dismissal is opt-in Ruleset with approval on the latest push and no bypass; Actions barred from approving PRs
T4 TB5: persistent self-hosted runner Elevation of privilege A PR job leaves a modified toolchain for the next job on that machine High: a foothold in the staging VPC Medium: persistent runners keep state Ephemeral runners; a separate runner group for untrusted refs
T5 TB3, DF3: third-party actions Tampering A mutable tag is repointed at a commit that dumps secrets into the job log High: every secret in the job Low to medium: tj-actions/changed-files, March 2025 (CVE-2025-30066) Full commit SHA pins, enforced by the allowed-actions policy
T6 TB3, DF3: npm install in the deploy job Tampering A compromised package’s install script runs beside the deploy credentials, or the package ships in the image High: the role, or code in production Low to medium: Shai-Hulud spread through npm post-install scripts in 2025 Separate build and deploy jobs; npm ci against a committed lockfile
T7 TB4, DF4: OIDC token traded for deploy-prod Spoofing The trust policy matches sub with StringLike repo:acme/payments-api:*, so any branch or PR run can assume the role Critical: full deploy-prod permissions Medium: AWS documents this pattern, and its console defaults the branch to * StringEquals on the exact environment subject
T8 Runner: long-lived repo secrets Information disclosure NPM_TOKEN and DATADOG_API_KEY leak through T2, T5 or T6, or print in a form log masking misses Medium: the npm token can publish a backdoored SDK Medium: any job on any ref can read them npm trusted publishing (OIDC) instead of NPM_TOKEN; Environment-scoped secrets
T9 DF5: docker push to ECR Tampering A principal with ecr:PutImage overwrites the tag between build and deploy High: an unreviewed image in production Low: few principals can push Tag immutability; deploy by digest
T10 TB6, DF7: cluster pulls images Tampering The cluster runs any image, so a stolen kubectl credential runs unsigned code High: arbitrary code in production Low: needs cluster credentials first Sign in CI with cosign or AWS Signer; verify at admission
T11 TB6, DF6: helm upgrade against EKS Elevation of privilege deploy-prod is mapped to cluster-admin, so a compromised deploy job controls every namespace Critical: every team’s workloads Low: needs T2, T5, T6 or T7 first Namespace-scoped RBAC; later, pull-based GitOps
T12 DF4, DF6: deploy audit trail Repudiation A manual deploy, or a deploy-prod session from outside CI, can’t be tied to a person and commit Medium: slows every investigation Medium: nothing records the actor unless someone sets it up Run ID in role-session-name; logs in an account CI can’t write to
T13 Whole pipeline Denial of service Runner exhaustion or a GitHub Actions outage blocks a hotfix mid-incident Medium: an outage you can’t patch fast Low, but on your worst day Concurrency limits; a logged, tested break-glass deploy

The chain hiding in T2 and T7

GitHub’s OIDC reference gives a pull request run the subject repo:acme/payments-api:pull_request and a branch run repo:acme/payments-api:ref:refs/heads/<branch>. It says: “To control how your cloud provider issues access tokens, you must define at least one condition, so that untrusted repositories can’t request access tokens for your cloud resources.” Acme defined one. AWS’s IAM guide shows Acme’s exact pattern, repo:ORG/REPO:*, as a condition that “grants access to any branch within the repository.” The console defaults the branch field to *, and IAM’s create-time check rejects a sub only when it is “solely a wildcard character.” Acme’s policy passes every check and admits every branch.

GitHub won’t issue the token without id-token: write, but a branch’s workflow can grant itself that permission. One phished engineer, or one bad package in a branch build, reaches production AWS with a YAML edit and one STS call, and nobody reviews anything. Finding that chain is most of what this prompt tests.

Before your interview, run this table against your own team’s pipeline. A T2, T5 or T7 found there is your answer to “what have you caught in the wild?”

Ranked mitigations, and why that order

Rank on three questions, in order. Does the fix break a path from the least trusted actor to production? How likely is that path? What does the fix cost? The first question is why T5 and T6 (high impact, low to medium likelihood) outrank T1 and T4 (high, medium). Code review can’t catch a repointed tag or a poisoned package, while fixes one and two gate T1, and T4 lands in staging.

Fix first: pin the OIDC trust to a production Environment (T7, T2)

Change the deploy-prod trust policy to accept exactly one subject:

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:acme/payments-api:environment:production"
  }
}

Match what your repo emits. Per the same OIDC reference, repos created after July 15, 2026, and repos renamed or transferred after that date, use an immutable format with numeric IDs, which here would read repo:acme@<owner-id>/payments-api@<repo-id>:environment:production.

Then create a production Environment with deployment branches limited to main, and move NPM_TOKEN and DATADOG_API_KEY out of repo scope into it. A branch can still declare environment: production, but GitHub won’t run that job off main, and AWS won’t hand the role to any other subject. That breaks the chain for branch-originated T1 and T2, though not T6, which runs in the deploy job on main.

On Team, the main-only rule and the exact sub are the whole gate, and they still break T2 plus T7. GitHub’s environment docs say: “If you are on a GitHub Free, GitHub Pro, or GitHub Team plan, required reviewers are only available for public repositories.” On Enterprise, add a required reviewer and turn on Prevent self-review, so whoever triggers a deployment can’t approve it. That adds a human step Acme didn’t have, and interviewers will push on what it costs in deploy speed and rubber-stamping.

Fix second: make main as hard to change as production (T3)

After fix one, main is the only road to production. Add a ruleset: one approval, approval required on the most recent reviewable push, an empty bypass list. If the org setting “Allow GitHub Actions to create and approve pull requests” is on, a branch workflow can approve its own pull request and satisfy “one approval.” GitHub shipped this control in 2022 to protect “against a user using Actions to satisfy the ‘Required approvals’ branch protection requirement.” Turn it off, make GITHUB_TOKEN read-only by default, and require CODEOWNERS review on .github/workflows/.

Fix third: run code you didn’t write where there’s nothing to steal (T5, T6, T9)

In the tj-actions compromise (CVE-2025-30066), existing version tags were moved to a malicious commit, so repos that changed nothing ran it. GitHub described Shai-Hulud, the npm worm it learned of in September 2025, as “a self-replicating worm” that spread “via compromised maintainer accounts by injecting malicious post-install scripts into popular JavaScript packages.” Split deploy.yml into a build job with no cloud credentials and a deploy job that installs nothing. That stops an install script from stealing the role. It doesn’t stop a malicious package that ships inside the image, and signing later signs whatever was built. So install with npm ci against a committed lockfile, add --ignore-scripts where the build tolerates it, and pin actions to full commit SHAs, enforced by the SHA-pinning option in the allowed-actions policy. Add ECR tag immutability and digest deploys too.

Then shrink the blast radius (T1, T4, T11, T8)

T1 lands here: after fixes one and two a stolen session can push a branch but needs a second approver to reach main, and required 2FA is an org setting, not a pipeline fix. Make the self-hosted runners ephemeral, with untrusted refs on their own runner group. Scope the deploy identity with a namespace-level RBAC role instead of cluster-admin; longer term, pull-based GitOps with Argo CD or Flux keeps cluster credentials out of CI. If the SDK is on npmjs.com, replace NPM_TOKEN with npm trusted publishing, which uses OIDC from GitHub-hosted runners.

Later, or accept with eyes open (T10, T12, T13)

Provenance is now a workflow step. GitHub’s artifact attestations need Enterprise Cloud on private repos, so on Team sign with cosign or AWS Signer and Notation; SLSA describes what to record. Enforcing it at admission is where the cost sits, for a low-likelihood path once T9 is fixed; say so rather than leading with it. For T12, put the run ID in role-session-name. Accept T13 with a tested break-glass path.

If the interviewer says “you get three,” name fixes one to three and give the T2-plus-T7 chain as the reason.

The threat modeling interview format, minute by minute

A 60-minute slot splits like this.

Minutes into a 60-minute threat modeling interview What the candidate does What the interviewer is checking
0–5 Ask scoping questions; state assets and attackers Defining the problem before solving it
5–15 Draw components, number the data flows, label the trust boundaries Finding where trust changes, unprompted
15–35 Walk boundary crossings with STRIDE as a checklist; 10 to 15 threats Threats specific to this system, not recited
35–45 Stop listing; rate impact and likelihood; look for chains; pick a top three Telling the headline threat from the footnote
45–60 Defend fixes under pushback; residual risk, detection, your questions Depth, and updating on new facts

In 45 minutes, compress scoping, drawing and enumeration, never the ranking.

Scoping questions to ask before you draw

  • Is the worst outcome someone else’s code in production, or payment data walking out?
  • Who’s the attacker: an outsider with a stolen credential, an insider, a compromised dependency?
  • Which GitHub plan, is the repo private, and do forks open pull requests?
  • What’s in place today: SSO, branch protection, deploy approvals?

Draw boxes for things that run code, numbered arrows for data flows and a labeled line per boundary. Numbers pay off at minute 40, when “T2 plus T7” replaces re-describing two arrows. Keep talking while you draw.

When to stop enumerating

Stop when every boundary has a threat and you’ve found one path from the least trusted actor to the asset, or at minute 35, whichever comes first. Say it: “I have twelve. I’ll rank these unless you want me deeper somewhere.” Run the six letters once before you stop; people forget repudiation and denial of service.

How interviewers grade a threat model

Amazon’s page names the competency without saying how it’s scored, and few companies publish a rubric, so treat this as a practitioner’s read.

Say assumptions out loud: “I’m trusting GitHub and AWS as platforms” lets the interviewer redirect you in ten seconds. Threats are judged on specificity. “An attacker could compromise CI” gives them nothing to grade, while “a branch workflow reads NPM_TOKEN and publishes a backdoored SDK” names who does what, on a flow that’s on the board. Ranking needs a stated rule, and mitigations are graded as configuration: which setting, owned by whom. “Add MFA” scores close to nothing.

Follow-up questions interviewers ask

“Make the repo public. What changes?” Fork pull requests don’t get repo secrets, so T2 narrows, and required reviewers become available on every plan. You’ll want one. Since December 8, 2025, pull_request_target takes its workflow file and checkout from the default branch, and Environment rules evaluate against it. So if main already has a pull_request_target job that checks out fork code and declares environment: production, the main-only rule passes, the sub matches, and only a required reviewer stops a stranger’s code from running with the production role.

“How would you know T7 had already been used?” Match each CloudTrail AssumeRoleWithWebIdentity event for deploy-prod to an expected workflow run on main. A session without one is your incident.

Failure modes that sink candidates

The most common is listing attacks before drawing anything, which produces the OWASP Top 10 and nothing about this system. Next is filling in the STRIDE grid: ten elements times six letters is sixty cells, and the round ends before you rank.

Some candidates defend the system. When the interviewer plays the owner and says “we already have branch protection,” ask what it protects; that’s how you find T2. Others hand-wave the platform: if you name GitHub Actions, know where workflow files come from and what your plan includes. The GitHub Actions system design walkthrough covers the platform.

Other threat modeling interview questions, and where the chain hides

The method carries to other common prompts; for the app-layer ones, see web security for system design interviews.

  • Login and password reset: the reset token, and whether reset quietly skips MFA (the user authentication design has the moving parts).
  • S3 presigned upload: who picks the object key and content type, and whether uploads are served from your origin.
  • Mobile app plus API: any check the app enforces that the API doesn’t repeat, since the attacker owns the client.
  • Internal admin tool: SSO at the front door and no per-action authorization behind it.
  • An LLM agent with a repo-write token and a browsing tool: a fetched page injects instructions, and the agent pushes code with its token.

In each, the chain runs through the component that acts on input it didn’t choose. Circle that box first.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack →
Scroll to Top