Repository navigation
v18.0.0
Otoroshi version 18.0.0
Otoroshi 18 is the largest release we have shipped in years, and probably the most carefully prepared one. The whole runtime was rebuilt on new foundations, Service Descriptors are finally gone, the way APIs identify their consumers was redesigned, and every corner of the gateway went through a security audit.
It took more than four months, from the first commit of the Scala 3 port in June to today: around 1,000 commits, 234 issues closed, 9 previews and 4 release candidates. The rule for the port was strict: change the foundation, change nothing else. New features only came afterwards, one preview at a time, on a base that had already proven it could carry the old weight. Along the way the test suite grew by more than 70,000 lines (clustering, mTLS, WebAuthn, SAML, admin API rights, JSON round trips of every entity, OpenAPI conformance) so that this release could be checked, not just hoped for.
For most installations, upgrading should be boring: your routes, plugins, API keys, certificates and exporters are untouched. The full story is in the Otoroshi 18 blog post.
Highlights
- New foundations: Scala 2.12 → Scala 3.8, Play 2.8 → Play 3.0, Akka → Apache Pekko 1.6. No more patched or vendored third party jars, back on supported upstreams. The admin UI moved from webpack to Vite and jQuery is gone.
- Service Descriptors are removed. On first startup, a job backs them up to
./service-descriptors-backup.jsonand converts each one to a Route with the same id. A descriptor is only deleted once its route has been written and read back, anything that fails is left alone, logged and retried at next startup. See Sunsetting Service Descriptors. - Every API consumer is an API key, whatever the plan (
keyless,jwt,mtls,oauth2-remote, ...), so quotas, rate limits and analytics apply to every caller. See API consumers and plugin flows. - Plugin chains per consumer: a plan or a single API key can bring its own plugins, on top of the route chain or instead of it.
- Conditional plugins: run any plugin only when JSONPath predicates match on the API key, the user, the route, the request or the response. See Conditional plugin.
- Security pass: two full audits of the codebase, and a long list of security issues fixed (admin API and backoffice rights, tenant and team scoping, auth. modules, plugin hardening, secrets in exported events, ...). SAML validation is secure by default. We also wrote a production checklist, worth ten minutes before any install or upgrade.
- Trusted proxies: the client address is now resolved from the trusted end of the
X-Forwarded-Forchain once your reverse proxies are declared, and the effective configuration is logged at startup. - Admin API OpenAPI document: rewritten, with updated schemas and examples, and checked against a running Otoroshi by a conformance suite. The Swagger UI plugin now ships its own bundle.
- Performance: a full pass on the request hot path (expression language, reporting, metrics, timeouts, id generation), and the JVM is now sized from the container limits in the Docker image, the Helm chart and the Kustomize manifests.
- And also: OpenSearch support, ACME chain selection, renewal margin and duplicates cleanup, Redis TLS material from config, WebAuthn working again, Bitbucket Server remote catalogs, a working Brotli plugin on Apple Silicon, configurable User Analytics events.
On top of that, a huge number of bugs were fixed along the way. They are all listed below.
Before you upgrade
| If you... | then... |
|---|---|
| run Otoroshi on Java 11 | Java 17 or later is required, jdk11 images are no longer built |
| run the Otoroshi jar on your own JDK | update that JDK, or block the compromised eMudhra intermediate CAs as the OpenJDK advisory explains (CVE-2026-91403). The Docker images already block them |
tuned akka.* settings |
rename them to pekko.*. OTOROSHI_AKKA_* env. variables keep working, OTOROSHI_PEKKO_* ones are available |
| ship custom Scala plugins as jars | recompile them against Scala 3 and fr.maif:otoroshi_3 (was otoroshi_2.12). WASM plugins are unaffected |
| deploy on Kubernetes | apply the new CRDs: the ServiceDescriptor CRD is gone and the ingress.otoroshi.io/is-route annotation is no longer read |
publish APIs with keyless, jwt, mtls or oauth2-remote plans |
callers now become API keys, persisted by default. On a public API with many distinct callers, set create_if_missing to false on the plan |
| use SAML auth. modules | responses are really checked against the issuer, must carry exactly one assertion, and must be signed when signature validation is on |
rely on the status code of NgServiceQuotas, NgCustomQuotas or NgCustomThrottling |
they now answer 429 instead of 403 |
| delegate admin rights to tenant or team scoped users or API keys | nobody can grant rights they do not hold, or move an entity out of their scope anymore |
| start or stop an API through the admin API | _start and _stop now require a POST |
If you still have Service Descriptors outside the datastore (Git, CI, manifests), POST /api/routes/_from_service_descriptor converts them to routes without storing anything. The 17.x line stays available for anyone who needs more time.
Thanks to everyone who tested the previews and release candidates, reported issues and contributed code. Please keep telling us what you find, on GitHub or Discord.
https://github.com/MAIF/otoroshi/milestone/141?closed=1
v18.0.0-rc4...v18.0.0
https://github.com/MAIF/otoroshi/releases/tag/v18.0.0
Added
- Helm values with annotations for fixed IPs/IDs (#2631 fixed by )
- Let user choose the preferred root for ACME certificates (#2621 fixed by mathieuancelin)
- Certificate CRD generated by Helm should have optional public domains (#2628 fixed by mathieuancelin)
- Provide Redis custom CA cert (lettuce) from config. and env. variables instead of java *stores only. (#2616 fixed by mathieuancelin)
- Otoroshi not compatible with opensearch. (#2630 fixed by mathieuancelin)
- migrate to play 3.0.0 (#1755 fixed by mathieuancelin)
- clustering test kit (#1292 fixed by Zwiterrion)
- Removing service descriptor inputs/outputs (#2645 fixed by mathieuancelin)
- upgrade webauthn to make it work again (#1338 fixed by mathieuancelin)
- Add support for custom plugins flow per apikey (#2646 fixed by mathieuancelin)
- Change Api access strategy by using apikey for every plan kind (#2648 fixed by mathieuancelin)
- ACME certficate purge (#2649 fixed by mathieuancelin)
- Frontend regressions after the webpack to vite migration (#2643 fixed by Zwiterrion)
- Conditional wrapper for plugins (#2653 fixed by mathieuancelin)
- Open user analytics to additional queries and projections from extensions (#2656 fixed by mathieuancelin)
- let user select captured events in
User Analyticsdata exporter (#2657 fixed by mathieuancelin) - Support trusted proxies and extract trusted ip address (#2669 fixed by mathieuancelin)
- Use proxyState and introduce cache for JWKS documents (#2685 fixed by mathieuancelin)
Changed
- ensure the main executor for the gateway is still in the health path (#2639 fixed by mathieuancelin)
- Remove documentation from maven publish to avoid hitting maven central publish quotas (#2642 fixed by mathieuancelin)
- Update build toolchain for documentation (#2651 fixed by mathieuancelin)
- Optimizations for Json path evaluation (#2654 fixed by mathieuancelin)
APIs: streamline API configuration with (#2480 fixed by )- fix
createOtoroshiRouteflaky creation check (#2670 fixed by mathieuancelin) - Log the effective trusted proxies at startup (#2676 fixed by mathieuancelin)
- The client address is not resolved once per request, contrary to what
ipSafe(attrs)documents (#2680 fixed by mathieuancelin) - Global performance improvements (#2689 fixed by mathieuancelin)
- Remote catalogs (GitOps): an Api cannot be managed by a catalog (#2721 fixed by mathieuancelin)
- Experimental certificates by domain endpoint exposes every private key (#2738 fixed by mathieuancelin)
- The default claim shared key is not reported at startup (#2766 fixed by mathieuancelin)
Fixed
- NgPluginHttpResponse swallows response chunking (#2617 fixed by mathieuancelin)
- Helm chart display wrong message about DNS binding in cluster mode (#2619 fixed by mathieuancelin)
- Investigate issue with RDP over websocket (#2623 fixed by mathieuancelin)
- Websocket chunk size is fixed when using netty server (#2627 fixed by mathieuancelin)
- Docker entrypoint does not pass args to the java process (#2632 fixed by mathieuancelin)
- Elastic data exporter reports a success even when the cluster rejects the events (#2635 fixed by mathieuancelin)
- Out of the box helm install seems to have broken otoroshi-admission-webhook-validation (#2637 fixed by mathieuancelin)
- ACME certificate duplication (#2650 fixed by mathieuancelin)
- NgServiceQuotas, NgCustomQuotas and NgCustomThrottling return bad status code when reaching limits (#2655 fixed by mathieuancelin)
- brotli compression plugin does not work on apple silicon (#2677 fixed by mathieuancelin)
ForwardedHeaderplugin emits aForwardedheader that does not follow RFC 7239 (#2673 fixed by mathieuancelin)RevokedApiKeyUsageAlert:fromandfromOriginuse two different addresses (#2674 fixed by mathieuancelin)- Align address matching in IP filters and trusted proxies (#2675 fixed by mathieuancelin)
- Helm replicaCount doesn't override default values in mode Cluster (#2682 fixed by mathieuancelin)
- The OpenAPI schemas describe the case classes, not the JSON the API reads and writes (#2683 fixed by davlgd)
- Missing units on bandwidth throttling plugins config forms (#2701 fixed by mathieuancelin)
- Remote catalogs: entities of a kind that is no longer in the source are never deleted (#2706 fixed by mathieuancelin)
- Remote catalogs: entities refused by the write validation are reported as created or updated (#2707 fixed by mathieuancelin)
- WorkflowWebsocketTransformer runs the incoming workflow on outgoing messages (#2708 fixed by mathieuancelin)
- NgCustomThrottling reads
perRoutebut writesper_route: per_route=false is lost (#2709 fixed by mathieuancelin) - NgCustomQuotas: the daily counter expires after app.throttlingWindow (10 s), not at the end of the day (#2710 fixed by mathieuancelin)
- Some NG plugin configs do not survive a write/read round trip (#2711 fixed by mathieuancelin)
- StaticResponse prints its header templates to stdout on every request when apply_el is on (#2712 fixed by mathieuancelin)
- ApiConsistencyService: a plan change reaches every subscription, and a removed plan deletes none (#2722 fixed by )
- Remote catalogs (GitOps): an Api cannot be managed by a catalog (#2721 fixed by mathieuancelin)
- Backoffice workflow test endpoint runs workflows without checking user rights (#2724 fixed by mathieuancelin)
- Basic auth. from auth. module: a cached LDAP login is reused without checking the password (#2725 fixed by mathieuancelin)
- API entity admin endpoints: lifecycle actions with read rights only, openapi imports without rights check (#2727 fixed by mathieuancelin)
- OIDC access_token validator: an opaque token is trusted without validation when fetch_user_profile is off (#2726 fixed by mathieuancelin)
- Legacy Client Certificate + Api Key only plugin matches the issuer DN instead of the subject DN (#2729 fixed by mathieuancelin)
- Remote catalogs: deploy, undeploy and dry run check no rights on the catalog (#2728 fixed by mathieuancelin)
- Tenant admin can discard the private apps sessions of an auth. module of another tenant (#2730 fixed by mathieuancelin)
- OpenAPI document: unbounded cache keyed by the extension_group query parameter (#2731 fixed by mathieuancelin)
- Backoffice authorized entities of an apikey: names entities the user cannot read (#2732 fixed by mathieuancelin)
- Apikey rejection reason of gateway events can carry the client secret (#2733 fixed by mathieuancelin)
- Workflow debugger websocket ignores the backoffice users blacklist (#2734 fixed by mathieuancelin)
- OpenAPI document: an unknown extension_group is still computed on every request (#2735 fixed by mathieuancelin)
- API entity: _start and _stop change state on a GET (#2736 fixed by mathieuancelin)
- Admin extensions: declaring an override hides the regular extension routes (#2737 fixed by mathieuancelin)
- Admin api certificate renewal checks no rights (#2739 fixed by mathieuancelin)
- Paused workflow sessions admin api routes check no rights (#2740 fixed by mathieuancelin)
- Tunnel websockets accept unauthenticated agents and relays (#2741 fixed by mathieuancelin)
- Service stats and events endpoints check no read rights (#2742 fixed by mathieuancelin)
- Backoffice try it uses routes and client certificates the user cannot read (#2743 fixed by mathieuancelin)
- Small backoffice endpoints check no rights (#2744 fixed by mathieuancelin)
- Backoffice rights check runs the guarded block before checking (#2745 fixed by mathieuancelin)
- Log4Shell detection parser has an exponential cost (#2763 fixed by mathieuancelin)
- Typed admin api bulk writes can replace entities the caller cannot write (#2764 fixed by mathieuancelin)
- Creating from a template can replace an entity the caller cannot write (#2765 fixed by mathieuancelin)
- A tenant admin can write an admin api apikey without rights metadata (#2767 fixed by mathieuancelin)
- The otoroshi session cookies are forwarded to the backends (#2768 fixed by mathieuancelin)
- The response cache shares the responses to requests carrying credentials (#2769 fixed by mathieuancelin)
- Some plugins read request bodies in memory without any bound (#2770 fixed by mathieuancelin)
- Exported alerts and flow reports carry the secrets of the route and the request (#2771 fixed by mathieuancelin)
- An apikey validated for one candidate route is reused for another route (#2772 fixed by )
- The apikey plugin forwards some credentials to the backend despite wipe_backend_request (#2773 fixed by mathieuancelin)
- A not match rule of the allowed users plugin lets every other user in (#2774 fixed by mathieuancelin)
- Request data evaluated in a plugin config can rewrite the config (#2775 fixed by mathieuancelin)
- The rate limiter evaluates the bucket key a second time (#2776 fixed by mathieuancelin)
- The OAuth2 token exchange cache ignores the auth module, the client and most exchange parameters (#2777 fixed by mathieuancelin)
- An apikey set by another plugin skips every check of the apikey plugin (#2778 fixed by mathieuancelin)
- Client certificate rules match any certificate of the chain sent by the client (#2779 fixed by mathieuancelin)
- The legacy client certificate matcher checks a fake certificate when none is sent (#2780 fixed by mathieuancelin)
- An HMAC signature can be replayed on any request (#2781 fixed by mathieuancelin)
- The fail_if_present option of the JWT signer is inverted (#2782 fixed by mathieuancelin)
- An HTTP signature without created never expires (#2783 fixed by mathieuancelin)
- A discovery registration on a route can target another route, a glob unregisters everything (#2784 fixed by mathieuancelin)
- The generic admin api checks the rights on tenants and teams against default/default (#2785 fixed by mathieuancelin)
- The Izanami proxy lets another context than the forced one through (#2786 fixed by mathieuancelin)
- The OIDC auth token plugin reads the session max age in milliseconds (#2787 fixed by mathieuancelin)
- Generic admin api can write admins with more rights than the writer (#2746 fixed by mathieuancelin)