Case study · Personal project
Aegis
A just-in-time access system: ask for what you need in plain English, get a decision from policy, and lose the access again when it expires.
The problem
Access tends to pile up. Someone needs the production database for an afternoon, gets it through a ticket, and still has it a year later, because removing it is a second manual step nobody owns. The safer pattern is just-in-time access: grant exactly what's needed, for as long as it's needed, and take it away automatically. Aegis puts that behind a plain-English request, so asking stays as easy as sending a message.
What it does
- Plain-English requests: "Need admin on prod-k8s-cluster for 6 hours to roll back a bad deploy" becomes a structured policy: resource, action, reason and duration.
- A policy decision with reasons: Cedar policies check the requester's role and department against the resource and return ALLOW or DENY, naming every rule that blocked the request.
- Time limits by sensitivity: grants are capped at 72 hours for public resources, 24 for internal, 8 for confidential and 2 for restricted.
- MFA step-up for the riskiest actions: restricted access, break-glass and approving anyone's access need a second factor from the last 15 minutes. Without one, Aegis asks you to step up (RFC 9470) instead of refusing, using the identity provider's MFA or a built-in authenticator.
- A second person for risky access: restricted resources, and anything that deletes or administers, wait up to 24 hours for an eligible approver.
- Break-glass: emergency access skips the wait but still has to pass the policy, is capped at 1 hour, and stays in the approvers' queue until someone reviews it.
- Automatic revocation: grants end when they expire, when revoked early, or when the person leaves, moves department or changes role.
- Provisioning from the identity provider: Okta or Entra ID create, update and deactivate people over SCIM 2.0, so offboarding someone there revokes their access in Aegis at once.
- Recertification: an auditor can ask the people who approve access to re-confirm every active grant; anything not confirmed by the deadline is revoked.
- Short-lived AWS credentials: an approved grant can be exchanged for STS credentials limited to that one action on that one resource, which expire with the grant.
- A tamper-evident audit trail of every request, decision, approval and revocation, with an endpoint that checks it hasn't been edited, shortened or rewritten.
- Policy you can test: a suite of policy test cases runs in CI, and a what-if simulator answers questions like "what if Alice moved to Finance?" without changing anything.
- Detection and review: rules flag prompt-injection attempts, privilege-escalation attempts and break-glass use, and an access review report gives auditors per-person recommendations and control checks.
How it's built
A FastAPI service on SQLite through SQLAlchemy 2, with Pydantic v2 models and an APScheduler job that sweeps for expired grants and stale approvals every minute. Requests are parsed by Claude with structured outputs, so every reply validates against the policy schema; without an API key, a keyword parser takes over. Decisions come from Cedar, the policy language behind AWS Verified Permissions, with policies checked against a schema at startup. Sign-in is by JWT: Aegis can issue its own tokens for local use, or verify tokens from an external identity provider (Keycloak, Okta, Entra ID, Auth0) against its published keys, with the dashboard signing in through the authorization code flow and PKCE.
A pytest suite of 186 tests (93% coverage) covers the policy rules, the approval workflow, MFA, SCIM, authentication, the audit chain and the AWS broker (against a mocked AWS), including tests that hijack the parser to prove the policy still holds. CI also runs Ruff, mypy, Bandit, pip-audit and CodeQL, and signs in to the dashboard through a real Keycloak server, with a password and a one-time code, on every push.
Key decisions
The model reads, the policy decides
Claude only extracts what was asked for; its instructions say not to judge, soften or refuse, because a separate engine makes the decision. Structured outputs guarantee the reply fits the schema, a resource that isn't in the catalog becomes "unknown" and is denied, and the request text is treated as untrusted data, so instructions hidden in it can't change the rules. The decision itself comes from Cedar policies that can be read, validated and tested.
Step up, don't just deny
A stolen session token shouldn't be enough to reach production. Requests for restricted resources, break-glass and approvals need a second factor from the last 15 minutes, checked from the identity provider's amr and auth_time claims. The check is a Cedar rule like any other, but when it's the only thing in the way, Aegis answers with a step-up challenge rather than a denial, and a request that would be denied anyway is simply denied, so the prompt never hints that it would have succeeded.
Separation of duties
Nobody approves their own request. An approver must be the requester's manager, a manager in the department that owns the resource, or a security engineer. Identity admins and auditors can't approve, so provisioning, approving and auditing stay with different people, and being an admin grants no access to resources at all. The policy is checked again when the approver acts, in case the requester's role changed in the meantime.
An audit trail that shows tampering
Each audit entry stores the previous entry's hash and an HMAC-SHA256 of itself and that hash, keyed with a secret kept outside the database. Editing, deleting or reordering a row breaks the chain from that point, someone with only database access can't rebuild it, and a unique constraint on the previous hash stops two writers from forking it.
A chain alone misses one thing: delete the newest entries and what's left still verifies. So Aegis also signs checkpoints with a separate Ed25519 key, "the log had this many entries, ending in this hash", and keeps them outside the database. Truncating the log, or even rewriting it with the HMAC key, no longer goes unnoticed.
Identity comes only from the token
Who is asking is never taken from the request body. Tokens are matched to people by their email claim, deactivated accounts are refused, and tokens from an external provider must be RS256 or ES256 with a valid issuer, audience and expiry.
Credentials that can be taken back
AWS STS credentials can't be recalled once issued. Each session is named after its grant, so revoking early adds a deny rule for that one session name to the role, which blocks credentials already handed out; once they would have expired anyway, the rule is cleaned up.
A live demo with no server
The demo runs the real service in your browser: Pyodide (Python compiled to WebAssembly) loads the Aegis source, and a small bridge hands each request straight to the FastAPI app. Cedar's own WebAssembly build makes the decisions, so they match the server's, and CI checks that they do, by running the policy test suite against both. I wrote up how the in-browser demo works. It swaps in the keyword parser, since a public page can't hold an API key, runs the scheduler's sweep from the page, and adds a clock you can skip forward to watch grants expire.
Current limits
It runs on SQLite, so it's a single-node service, and its rate limits are per process. Its built-in token issuer lets anyone sign in as anyone and is meant for local use only; a real deployment points it at an identity provider and sets its own keys. Audit entries written since the last signed checkpoint can still be deleted unnoticed, which is why they're also streamed to a SIEM. The live demo uses the keyword parser rather than Claude, and starts over whenever the page reloads.