Skip to main content

Route Protection

Evan Phoenix

The internet is a public place, and over time every app grows a layer of protections around it. A password gate goes up in front of the staging environment after the wrong person stumbles onto it. SSO gets bolted onto the internal dashboard once the team is big enough to care who’s who. A filter for the obvious attack traffic gets written the day someone notices the access logs. None of this work is the app, but it accumulates on top of the app, because the request lands there and the request is where you can do something about it.

That work belongs in front of the app, not inside it. Today we’re shipping Route Protection: a set of features that live at the ingress layer, declared with one command, with your app none the wiser.

There are three things in the box at launch: shared-password auth, single sign-on via OIDC, and a built-in WAF. They’re different features, but they share a shape — they all sit between ingress and your app, and they all attach to a route the same way.

Come along with me as I demo route protection:

Start with the simplest thing: a password

Before talking about identity providers and JWT claims, let’s talk about the case that’s actually most common: you have a thing on the internet and you’d like there to be a password in front of it.

A draft of a blog post. A staging environment. An internal admin page. A demo for a customer. The bar isn’t “this needs SSO” — the bar is “I don’t want random people on the internet seeing it.”

Two commands:

miren auth provider add-password staging-gate
# Enter password: ********

miren route protect blog.example.com --provider staging-gate

That’s the whole thing. Visit blog.example.com and you get a login form. Type the password, you’re in for 24 hours. Your app never sees the unauthenticated traffic — Miren handles the form, the session cookie, the rejection of bad attempts. There’s nothing to install in your app and nothing to wire up.

The password is stored as a bcrypt hash. Sessions are signed and encrypted. You can rotate the password or remove the protection entirely (miren route unprotect) without touching your app.

This is the feature that I personally wanted most. Most platforms make you pick between “completely public” and “set up real auth,” and the gap between those two is enormous. The space in the middle — “I trust the half-dozen people who know the password, that’s enough for now” — is where a huge amount of work actually lives.

Step up: single sign-on

When the password-on-a-sticky-note model stops being enough — when you want SSO against your company’s identity provider — you swap the provider type and keep the rest of the workflow.

miren auth provider add my-google \
  --provider-url https://accounts.google.com \
  --client-id $GOOGLE_CLIENT_ID \
  --client-secret $GOOGLE_CLIENT_SECRET \
  --scope email --scope profile

miren route protect myapp.example.com \
  --provider my-google \
  --claim-header email:X-User-Email \
  --claim-header name:X-User-Name

Unauthenticated requests get redirected to Google. After login, your app receives the user’s identity as plain HTTP headers — X-User-Email, X-User-Name, whatever claims you mapped. No OAuth library. No JWT validation in your app. No callback handler to write.

OIDC is the underlying protocol, so any standards-compliant identity provider works. Google and GitLab work directly. Self-hosted Keycloak works directly. GitHub needs a federation layer like Dex (it doesn’t expose a native OIDC endpoint), but the Miren side of the configuration is the same either way.

The model here is “trust the proxy.” Your app trusts the X-User-Email header because Miren is the only network path into the sandbox — there’s no way for an external client to set that header and reach your app directly. This is the same pattern OAuth2 Proxy, Traefik ForwardAuth, and nginx auth_request use, and it works well when the platform owns the network topology. Miren does, because of sandbox isolation, so your app gets to treat those headers as trusted input from the platform.

A nice side effect of this trust model: the same provider can sit in front of any number of routes. Set up my-google once, attach it to app1.example.com, app2.example.com, admin.example.com. The providers are reusable.

WAF: the other half of “protection”

Auth decides who gets through. The other half of the question is what gets through, regardless of who’s sending it. That’s the job of a WAF — a Web Application Firewall. It sits in front of your app and inspects each incoming request for known attack patterns, dropping the bad ones before they reach your code.

miren route waf myapp.example.com --level 1

Under the hood, Miren is running Coraza with the full OWASP Core Rule Set. That covers the standard catalog of web attacks: SQL injection, cross-site scripting, path traversal, command injection, request smuggling, and so on. Malicious requests get a 403 before they ever reach your app.

The --level flag is the OWASP paranoia level, 1 through 4. Level 1 catches the obvious stuff with very few false positives — that’s the right starting point for almost everyone. Higher levels catch more, at the cost of occasionally flagging weird-but-legitimate requests. If you’re protecting something that handles sensitive data, bump it up.

A few honest notes about scope. The WAF inspects request content for attack payloads. It does not rate-limit, fingerprint bots, or block reconnaissance scans (the /wp-admin/ probes, the /xmlrpc.php traffic). Those are real concerns, but they’re a different category of filtering, and an upstream proxy like Cloudflare in front of your route is still the right tool for them. The WAF is for “is this specific request trying to attack the app.”

When you have both auth and WAF on the same route, the order is: WAF inspects the request first, then auth decides who you are, then the request proxies to your app. The more skeptical check runs first.

Why this is a category, not three features

The reason all three of these features ship under one banner is that they’re the same shape. They’re all middleware that lives between ingress and your app. They all attach to a route with one command. They all have the property that your app doesn’t have to know they exist.

That shape has more room in it. Path-scoped protection (protect /admin but leave / public) is the next obvious step. Pass-through auth, where the identity headers are forwarded even on public routes so logged-in visitors get a richer experience without the route gating anything, is a natural extension. We’re also working on making miren.cloud itself usable as an identity provider, so you can do SSO without first standing up an OAuth application somewhere.

Whatever lands next, the interface stays the same: declare what protection a route gets, deploy, done.

Try it

If you’re already running Miren, all three features are available now. Add a provider, protect a route, turn on the WAF:

miren auth provider add-password my-gate
miren route protect blog.example.com --provider my-gate
miren route waf blog.example.com

The full details — provider examples, claim mappings, WAF paranoia levels, session behavior — live in the route protection docs and the WAF docs.

If you haven’t tried Miren yet, this is a good excuse to get started. Everything you read above runs on your own infrastructure, with no third-party auth service in the middle.