Inspiration

Cloud breaches rarely start with a sophisticated zero-day. They start with a public-read S3 bucket. A security group open to 0.0.0.0/0. A wildcard IAM policy that was "just temporary." These aren't exotic vulnerabilities — they're copy-paste errors that slip through code review because static scanner output is terse, jargon-heavy, and easy to ignore when you're shipping fast.

Tools like Checkov exist to catch these. But raw Checkov output tells you what is wrong without explaining why it's dangerous or exactly how to fix it. The gap between "flagged by scanner" and "understood and resolved" is where most findings die.

The deeper problem: an AI agent that could actually close that gap — fetching real code, explaining each issue, generating fixed files — needs real credentials to do it. And handing a GitHub token to an LLM agent is terrifying. A repo-scoped token can push branches, open pull requests, and delete tags. You don't want that living in an environment variable, in application logs, or anywhere near an LLM's context window.

Auth0 Token Vault solved this. It provides the identity layer that lets an AI agent act on behalf of a user — fetching real private repos, using real credentials — without the agent ever holding those credentials. That combination is what made IaC Sentinel possible.


What It Does

IaC Sentinel is an AI-powered security scanner for Infrastructure-as-Code. Users log in with Auth0, connect their GitHub account through Token Vault, and point the agent at any repository. In ~60 seconds they get a full security report with severity-rated findings and corrected file contents ready to copy.

The agent follows a strict five-step workflow:

  1. Fetch — exchanges the user's refresh token via Auth0 Token Vault for a short-lived scoped GitHub token, recursively collects all .tf, .yaml, .yml, and .json IaC files (up to 15 files), then discards the token immediately
  2. Scan — runs Checkov static analysis in a sandboxed temp directory, producing structured JSON findings
  3. Explain — DeepSeek LLM receives both the Checkov output and the raw code, returning structured findings: [High/Medium/Low] resource — description, risk context, and a one-line remediation per issue
  4. Fix — for every High-severity issue, generates a complete corrected version of the affected file — not just a diff, the whole file, ready to apply
  5. Preview — renders findings as filterable severity accordions and fix blocks as syntax-highlighted, copyable code

A live terminal tracks real-time progress from actual audit events. Results show severity stats, filterable findings, and a per-user audit trail that makes Token Vault's token activity visible. The entire credential lifecycle — OAuth consent, token exchange, rotation — is handled by Auth0. The agent never touches a raw GitHub token.


How We Built It

Architecture

Browser (Auth0 PKCE login)
        │
        ▼
FastAPI backend  ──  Auth0 Token Vault  ──►  GitHub API
        │                  ▲
        │          refresh token exchange
        │          (per tool call, discarded after use)
        ▼
LangGraph ReAct Agent (Grok-3-mini)
        ├── Tool 1: fetch_iac_files           →  Token Vault → GitHub
        ├── Tool 2: scan_iac_security_issues  →  Checkov + DeepSeek
        └── Tool 3: preview_fix_diff          →  structured diff output
        │
        ▼
Job store (Redis / in-memory fallback)
SSE stream → browser polling

Key decisions

Agent orchestration: LangGraph create_react_agent with Grok-3-mini as the orchestrator. Grok's only job is tool-calling and writing a single summary sentence — it never analyzes code. This keeps orchestration fast and cheap while putting all security reasoning on DeepSeek, which is optimized for it.

Token Vault integration: The agent calls exchange_for_github_token() inside fetch_iac_files — not at agent startup. Credentials are fetched on demand, scoped to the tool that needs them, and never passed between agent steps. The federated connection token-exchange grant:

{
  "grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
  "subject_token_type": "urn:ietf:params:oauth:token-type:refresh_token",
  "connection": "github"
}

Structured output contract: DeepSeek's prompt specifies an exact wire format — [High] resource — description lines with - Why it's risky: and - Fix: sub-bullets, followed by ### Fixed: path/to/file code blocks. The backend splits on these markers into findings_text and fix_blocks fields stored in the job record. The frontend never does string surgery on a raw LLM blob.

Security hardening: PII redaction on all LLM input and output. Path traversal prevention when writing temp files for Checkov. HMAC-signed job IDs so users can only poll their own scan results. Rate limiting at 5 scans/minute per IP. CSP, HSTS, X-Frame-Options, and Referrer-Policy headers throughout.


Challenges We Ran Into

The federated token exchange is not a standard OAuth flow. The grant_type URI is Auth0-specific, subject_token_type is Auth0-specific, and error responses don't map to HTTP status codes intuitively. A failed exchange returns invalid_grant whether the account isn't connected, the token is expired, or a parameter is wrong — you cannot distinguish the failure mode from the response alone. It took careful logging and manual testing to identify each case and raise ConsentRequiredError at the right layer.

DeepSeek output format drift. The security analysis prompt went through about a dozen iterations before it was stable. Findings need to be machine-parseable, so the prompt reads almost like a grammar specification: exact separator character (, not -), exact sub-bullet prefix (- Why it's risky:), explicit rule that no text appears before the first [High/Medium/Low] line. Early versions produced beautiful prose that was impossible to parse programmatically.

File path normalization across the LLM boundary. DeepSeek sometimes renamed files when generating fixed versions — main.tfterraform/main.tf, or stripped path prefixes entirely. The backend now does a two-pass normalization: direct 1:1 replacement if fix block count matches source file count, then basename matching as a fallback when filenames are unique.

Cross-tab OAuth session. The GitHub authorization page opens in a new tab via window.open. The auth_session token needed to survive the tab boundary via localStorage as a cross-tab message bus. One non-obvious subtlety: Auth0's connect flow has its own CSRF protection baked into auth_session. Adding a second state parameter validation layer breaks the flow — the security guarantee comes from auth_session, not from any parameter you manage. This is not clearly documented.

Blocking library calls in async FastAPI. Both PyGithub and Checkov are fully synchronous. Every GitHub traversal and every Checkov subprocess invocation had to be wrapped in asyncio.to_thread() to keep the event loop unblocked under concurrent scans.


Accomplishments That We're Proud Of

  • Zero plaintext tokens, ever. The GitHub token doesn't appear in environment variables, application logs, LLM context, or browser storage. It exists for the duration of one API call and is then gone.
  • Hybrid analysis that beats either tool alone. Checkov catches rule-mapped issues in milliseconds. DeepSeek explains why they're dangerous in plain English and generates context-aware corrected files. The combination produces findings that developers actually act on.
  • A frontend that makes the security story visible. The audit trail shows exactly when Token Vault exchanged credentials and for which job — users can see the access model in real time.
  • Production-ready, not just demo-ready. HMAC job binding, async locking, concurrent job limits, PII redaction, graceful ConsentRequiredError handling, and a live deployment on AWS Elastic Beanstalk.

What We Learned

Token Vault changes the mental model. You stop thinking "I have a GitHub token" and start thinking "I can get a GitHub token when I need one." That shift changes how you design credential handling entirely. A compromised server gives an attacker a refresh token they cannot exchange without your client_secret — and you can revoke it immediately from the Auth0 dashboard. Compare that to a GITHUB_TOKEN sitting in an environment variable.

Agent orchestration needs strict output contracts. The biggest source of bugs wasn't auth or infrastructure — it was LLM output format drift. Defining an exact wire format between models and testing it explicitly is as important as writing the system prompt itself.

The coordinator model should be fast; the analytical model should be deep. Grok-3-mini as orchestrator keeps the loop tight. Routing the hard security reasoning to DeepSeek keeps the analysis accurate. Mixing these roles degrades both.


What's Next for IaC Sentinel

  • Real PR creation gated behind a CIBA step-up consent prompt — _create_fix_pr() already implements the full GitHub flow, currently hard-coded to dry_run=True
  • Expose as an API endpoint for local-first sovereign agents so they can call IaC Sentinel as an authorized intermediary without ever holding GitHub credentials themselves
  • Scheduled org-wide scanning with a security posture dashboard and Slack alerts for High-severity findings
  • PR diff scanning via GitHub webhook — scan only the files changed in an open pull request, not the whole repo
  • Custom policy rules — let teams define Checkov custom checks stored alongside their Auth0 credentials

Bonus Blog Post

Submitted for the Bonus Blog Post Prize — materially different from the project description above.

What Auth0 Token Vault Gets Right — and Where It's Still Rough

I want to be honest about this, because honest product feedback from someone who actually built with a tool is more useful than another "Token Vault is amazing, here's my demo" post. IaC Sentinel uses Token Vault as its core security primitive. I think it's genuinely the right architecture for agentic credential management. I also hit some real sharp edges. Both things are true.

The Problem It Actually Solves

If you've built an agent that acts on behalf of users, you've already hit this wall. The agent needs credentials. Your options are:

  • Store a PAT in an environment variable — long-lived, unrevokable, one breach away from full repo access
  • Store OAuth tokens in your database — now you're a credential broker; implement encryption, rotation, revocation, and audit logging yourself
  • Pass tokens through the LLM context — the token appears in logs, potentially in fine-tuning data; attack surface is large

Token Vault offers a fourth option: Auth0 stores the provider token, your agent requests a short-lived scoped version at runtime, uses it once, and discards it. Your application is no longer a credential store. It's a credential requestor.

That mental model shift is the real value. The security properties follow from it automatically.

The Three-Token Dance

Worth understanding explicitly because the docs treat this as one flow when it's actually three distinct tokens:

1. Auth0 refresh token — issued at login, stored in an encrypted server-side session. The only long-term credential your application holds. Never sent to the browser.

2. My Account access token — short-lived, scoped to the Connected Accounts API (https://{domain}/me/). Used only when initiating or checking GitHub connections. Exchanged from the refresh token.

3. GitHub federated access token — retrieved at runtime per agent request, used once, discarded. Never stored anywhere.

The full federated exchange:

{
  "grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
  "client_id": "{your_client_id}",
  "client_secret": "{your_client_secret}",
  "subject_token": "{user_refresh_token}",
  "subject_token_type": "urn:ietf:params:oauth:token-type:refresh_token",
  "requested_token_type": "http://auth0.com/oauth/token-type/federated-connection-access-token",
  "connection": "github"
}

This is an Auth0 extension of RFC 8693. It's not in most tutorials. Getting it right took real iteration.

What It Gets Right

ConsentRequiredError as a first-class concept. When the exchange fails because the user hasn't connected GitHub yet — or their connection lapsed — the Auth0 SDK raises ConsentRequiredError with a re-authorization URL. In an agentic context this is genuinely useful: the agent catches it mid-scan and surfaces a "reconnect GitHub" prompt rather than returning an opaque 401. This is the right design.

Revocation is immediate and doesn't require user action. A compromised server exposes your refresh token. An attacker cannot exchange it without your client_secret. You revoke it from the Auth0 dashboard. Compare that to hunting down a forgotten GitHub PAT that's been in production for two years.

The consent model is honest. Users see a standard GitHub OAuth screen with explicit scopes. For agents that could eventually write to your repositories, this matters. The consent is recorded, revocable, and visible.

Where It's Still Rough

invalid_grant covers everything. A failed token exchange returns invalid_grant whether the GitHub account isn't connected, the refresh token is expired, the connection parameter is wrong, or the scopes don't match. There's no machine-readable sub-error code. In production you end up logging raw responses and manually categorizing failure modes. This should be fixed — account_not_connected, token_expired, and invalid_parameters are meaningfully different error states for an agent to handle.

The My Account token audience is non-obvious. The audience for the My Account API is https://{your_domain}/me/ — including the trailing slash, including the /me/ path. This is not prominently documented. It's the kind of thing you find by diffing working and broken requests in your network tab.

The auth_session CSRF protection is implicit. Auth0's connect flow manages its own CSRF via the auth_session token. If you try to add your own state parameter validation on top of the connect_code callback — which is a reasonable thing to do — it breaks the flow silently. The security guarantee comes from auth_session, not from any parameter you control. This should be explicitly called out in the docs.

The cross-tab UX requires custom plumbing. The GitHub authorization page opens in a new tab (or a popup). Getting the resulting connect_code back to the main application tab requires localStorage as a cross-tab message bus, a polling loop, and careful handling of the case where the callback lands in the main tab instead. None of this is hard, but it's also not provided. A TokenVaultConnectButton SDK component that handles all of this would remove a meaningful surface area of bugs.

The Pattern Worth Keeping

Despite those rough edges, I'd use Token Vault again. The alternative — building your own token broker with OAuth refresh, rotation, encryption, and revocation — is easily three times the work and introduces far more risk. Token Vault gets the hard parts right. The sharp edges are documentation and DX problems, not architecture problems.

The future of agentic AI is not agents that hold credentials. It is agents that borrow them — transparently, with user consent, at the moment they're needed, and return them immediately after. Token Vault is the infrastructure that makes that model practical to ship today. It just needs better error messages.

Built With

Share this project:

Updates