Your CI job has a secret. It's been sitting in your environment variables for two years. You don't know exactly who has access to it. Rotating it means coordinating three teams. So you don't.
That's not a process failure. That's what API keys are designed to be: long-lived strings that survive forever because they have to.
We're Moustafa Mahmoud Atta and Abd El-Sabour Ashraf, and we built OathMesh to change that default.
Every machine call gets a token that's cryptographically signed, scoped to a single action, and dead in ≤ 5 minutes.
The whole idea in two lines of HTTP
Before:
Authorization: Bearer abc123xyz_still_valid_since_2022
After:
Authorization: OathMesh eyJhbGciOiJFZERTQSIsInR5cCI6Im9tK2p3dCJ9...
└── expires in 300 seconds. enforced. not optional.
Leaked? By the time an attacker tries it, it's already dead.
Honest comparison
This is the real picture — no spin:
| Feature | API Keys | Typical JWT | Short-lived JWT + jti | OathMesh |
|---|---|---|---|---|
| Lifetime | ♾️ Forever | Hours–Days | 5–15 min (if configured) | ⏱️ ≤ 300s. Enforced. |
| Cryptography | ❌ Just a string | HS256 / RS256 | RS256 / ES256 | ✅ Ed25519 only |
| Replay protection | ❌ | ❌ | ✅ jti blocklist (DIY) | ✅ Built-in, required |
| Action scoping | ❌ | ⚠️ Custom, optional | ⚠️ Custom, optional | ✅ act required
|
| Policy engine | ❌ | ❌ | ❌ | ✅ Pkl rules, hot-reload |
| Audit log | ❌ | ⚠️ Roll your own | ⚠️ Roll your own | ✅ Every allow & deny, NDJSON |
| Leaked secret blast radius | 💀 Forever | 🩸 Hours | 🟡 Minutes | 🟢 ≤ 5 min. Max. |
| Rotation | 😰 Manual + coordination | ⚠️ Varies | ⚠️ Varies | ✅ Auto-expiry by design |
Honest take: You can get close to OathMesh with short-lived JWTs + a jti blocklist. What OathMesh adds is the opinionated wrapper: TTL enforcement you can't disable,
actscoping that's required (not optional), a built-in policy engine, and a full audit trail — out of the box, not DIY.Already running SPIFFE/SPIRE or cloud workload identity (AWS IRSA, GCP WI)? Great — those are excellent fits for Kubernetes-native setups. OathMesh is for teams who want this security model without the full service-mesh footprint. And if you want simpler than all of this, keep the API key. If you want safer, read on.
A real scenario: GitHub Actions → your deploy API
The CI job never stores a secret. It requests a token with a 300-second TTL, uses it, and it's gone. Even if someone captures it from your logs — they get nothing.
How verification works — 14 steps, fail-closed
Fail-closed means: if any single step fails, the request is rejected immediately. No partial-valid state. No fallback. Just 401.
| Step | What it checks |
|---|---|
| 1 | Valid JWS compact structure (3 segments) |
| 2 | Header: typ = om+jwt, alg = EdDSA
|
| 3 | Payload decoded, iss extracted |
| 4 |
iss in trusted issuer list |
| 5 | JWKS loaded (cached in-memory) |
| 6 | Ed25519 signature verified — no algorithm confusion possible |
| 7 |
iss re-verified post-signature |
| 8 |
exp is in the future (±10s clock skew tolerance) |
| 9 |
iat is not in the future |
| 10 |
aud matches exactly |
| 11 | All required claims present: sub, act, jti
|
| 12 | Request hash binding checked (if present) |
| 13 | jti checked against replay cache — seen before → rejected |
| 14 | Policy evaluated → audit event emitted |
Steps 6 and 13 are the heavy hitters. No algorithm confusion. No replay. No exceptions.
Gateway mode — protect services you can't modify
Already have APIs you can't change? Run OathMesh as a reverse proxy in front of them.
Your upstream gets clean, pre-verified identity headers. Zero code changes required.
Drop it into your stack in ~5 lines
Go (chi)
r.Use(middleware.OathMeshMiddleware(cfg))
// Fully typed caller context in your handler:
caller := middleware.CallerFrom(r.Context())
// caller.Principal.Subject → "agent://ci/deploy-bot"
// caller.Action → "deploy"
// caller.TokenID → unique jti for this call
Python (FastAPI)
from oathmesh import verify_token, OathMeshError
@app.post("/deploy")
async def deploy(request: Request):
try:
caller = verify_token(request.headers["authorization"], config)
except OathMeshError as e:
raise HTTPException(status_code=401, detail=str(e))
return {"deployed_by": caller.principal.subject}
Next.js (App Router)
import { withOathMesh } from '@oathmesh/oathmesh/next';
const oathmesh = withOathMesh({ audience, trustedIssuers });
export async function POST(request: NextRequest) {
const { caller, error } = await oathmesh(request);
if (error) return error; // typed 401 — missing, invalid, expired, replayed
return NextResponse.json({ subject: caller.principal.subject });
}
Full examples for Express, Flask, Django, and chi are in the quickstarts.
Try it in 3 commands
git clone https://github.com/oathmesh/oathmesh.git && cd oathmesh
docker-compose up -d
# Mint a token (300s = the maximum, enforced by the issuer)
TOKEN=$(docker compose exec oathmesh ./bin/oathmesh mint \
--sub "agent://repo/acme/deploy-bot" \
--aud "https://inventory.internal" \
--act "deploy" \
--ttl 300 \
--quiet)
curl -H "Authorization: OathMesh $TOKEN" http://localhost:8081/inventory
Or run ./demo.sh for the full golden-path demo end to end.
Honest pros and cons
What we got right ✅
- 300s max TTL is enforced in the issuer — there's no config flag to make it longer. Intentional, not an oversight.
- Ed25519 only — one algorithm, the correct one. Algorithm confusion attacks aren't possible.
- Fail-closed verification — all 14 steps must pass. No partial-valid state.
- Full audit trail — every allow and every deny logged as NDJSON. grep-able. Cloud-native.
- Gateway mode — zero changes to your existing APIs.
MIT license — take it, fork it, self-host it.
What we know needs work ❌It's v0.1.0 — rough edges exist. Read the threat model before running in production.
You need to run an Issuer service — one more thing to deploy and keep alive. Real operational cost.
Horizontal scaling needs Redis — the replay cache is in-memory by default. Multiple instances need a shared Redis. We're not hiding this.
Pkl for policies — powerful, but not everyone knows Apple Pkl. A visual editor is on the roadmap.
Machine-to-machine only — user auth is a different problem. Use OAuth2/OIDC for that.
- Your Ed25519 private key is your trust root — unlike an API key (which compromises one service), a leaked signing key compromises every service on your mesh. Store it in a secrets manager (Vault, AWS KMS, GCP Secret Manager) — not in an env var. This is the one rule that matters most.
Use OathMesh if
- You're running CI/CD pipelines that call internal APIs
- You have service-to-service calls in a zero-trust or service mesh setup
- You're building AI agents that call protected services
-
A leaked credential in your environment would cause real damage
Don't use OathMesh (yet) if
You need tokens that live longer than 5 minutes by design
You can't add infrastructure — the Issuer service is not optional
- You need user-facing authentication — this is not the tool
What's coming
| Feature | Status |
|---|---|
| Rust + Java SDKs | 🔜 Next release (v0.2.0) |
| mTLS + rate limiting in Gateway | 🗓️ Planned |
| Visual policy editor (no Pkl required) | 🗓️ Planned |
| Audit dashboard | 🗓️ Planned |
| GitLab CI + GitHub App issuers | 🗓️ Planned |
We built OathMesh because we kept hitting the same wall: leaked credentials with no expiry and no audit trail. The fix shouldn't require a security team or an enterprise budget.
It's early. It has rough edges. But the model is sound, the code is open, and the MIT license means you can take it wherever you need it.
If it solves a problem you have — or if you think we're wrong about something — open an issue or start a discussion. We genuinely want to hear from you.
🔗 github.com/oathmesh/oathmesh
npm install @oathmesh/sdk
pip install oathmesh
go install github.com/oathmesh/oathmesh/cmd/oathmesh@latest
Built by Moustafa Mahmoud Atta & Abd El-Sabour Ashraf — MIT License



Top comments (0)