Protect a private API with Otoroshi
Keep an application out of public reach, and let Otoroshi decide which requests get to it.
An API gateway sits in front of your services: it receives the requests, checks them, and forwards the valid ones. Otoroshi is the API gateway that Clever Cloud runs for you, with hundreds of plugins to authenticate, limit, and transform requests. These protections only matter if requests can’t bypass the gateway and reach the application directly, through its own domain.
This guide deploys a Node.js API and blocks its public domain with Request Flow. Otoroshi then reaches it through a Network Group, a private WireGuard network. It only forwards GET requests that carry a valid API key, within a quota, and bans for a while the clients that fail too often.
Prepare the application
You need Git, jq, OpenSSL, Clever Tools, and a Clever Cloud account. Create a project and initialize its Git repository:
mkdir private-api
cd private-api
git initThe application answers every request with its path, the address of its client, and the headers it receives, which show where requests come from and what reaches the application. It listens on 0.0.0.0, so that the Network Group reaches it, and on the port set in PORT:
const http = require('node:http');
const port = process.env.PORT || 8080;
http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ path: req.url, client: req.socket.remoteAddress, headers: req.headers }, null, 2) + '\n');
}).listen(port, '0.0.0.0');The start script of package.json tells the Node.js runtime how to run it:
{
"name": "private-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js"
}
}Deploy the private application
Create the application, then show its domain:
clever create --type node private-api
clever domainBlock its public access: with CC_REQUEST_FLOW set to block, its public domain answers 200 OK to every request, without reaching the application. The application then listens on port 9000, set in PORT:
clever env set CC_REQUEST_FLOW block
clever env set PORT 9000Commit the project, then deploy it:
git add .
git commit -m "Deploy a private API"
clever deployOnce deployed, the domain of the application answers OK to any request:
curl https://app-xxx.cleverapps.io/helloCreate Otoroshi and its Network Group
Create the Otoroshi add-on. It serves routes on its routes domain, by default a cleverapps.io domain. To choose it, add the routes-domain option at creation, such as --option routes-domain=my-api.cleverapps.io, or your own domain, whose DNS you configure.
Then enable the Network Group of Otoroshi with the operators commands of Clever Tools. Enabling it restarts Otoroshi:
clever addon create otoroshi my-otoroshi
clever features enable operators,ng
clever otoroshi enable-ng my-otoroshiOtoroshi now belongs to a Network Group. Get the IDs of the application and of this group, then link the application to it, so that Otoroshi reaches it on port 9000 through the domain name of its member:
APP_ID=$(jq -r '.apps[0].app_id' .clever.json)
NG_ID=$(clever otoroshi get my-otoroshi -F json | jq -r .features.networkGroup.id)
clever ng link "$APP_ID" "$NG_ID"
BACKEND="$APP_ID.m.$NG_ID.cc-ng.cloud"Configure the protections
clever otoroshi get gives the URL and the credentials of the API of Otoroshi. Otoroshi runs on a Java application, whose OTOROSHI_ROUTE_BASE_DOMAIN variable holds the routes domain:
OTOROSHI=$(clever otoroshi get my-otoroshi -F json)
API_URL=$(echo "$OTOROSHI" | jq -r .api.url)
API_AUTH=$(echo "$OTOROSHI" | jq -r '"\(.api.user):\(.api.secret)"')
OTOROSHI_APP=$(echo "$OTOROSHI" | jq -r .resources.entrypoint)
ROUTES_DOMAIN=$(clever env --app "$OTOROSHI_APP" -F json \
| jq -r '.env[] | select(.name == "OTOROSHI_ROUTE_BASE_DOMAIN") | .value')This route serves /api on the routes domain, and forwards requests to port 9000 of the application, with their full path. The WireGuard tunnel already encrypts this traffic, so the backend doesn’t use TLS. Its plugins check each request in turn:
Fail2BanPluginbans a client for a minute once it sends a wrong secret 3 times within a minuteAllowHttpMethodsonly acceptsGETrequestsApikeyCallsrejects requests without a valid API key, or beyond its quota
curl -u "$API_AUTH" -H "Content-Type: application/json" \
-X POST "$API_URL/api/routes" --data @- <<EOF
{
"id": "route_private_api",
"name": "private-api",
"frontend": { "domains": ["$ROUTES_DOMAIN/api"], "strip_path": false },
"backend": { "targets": [{ "hostname": "$BACKEND", "port": 9000, "tls": false }] },
"plugins": [
{ "plugin": "cp:otoroshi.next.plugins.OverrideHost" },
{ "plugin": "cp:otoroshi.next.plugins.Fail2BanPlugin", "config": {
"max_retry": 3, "detect_time": 60000, "ban_time": 60000, "status_codes": ["401"] } },
{ "plugin": "cp:otoroshi.next.plugins.AllowHttpMethods", "config": { "allowed": ["GET"] } },
{ "plugin": "cp:otoroshi.next.plugins.ApikeyCalls" }
]
}
EOFThis API key only works on this route, with a quota of 10 calls every 10 seconds, the default throttling window of Otoroshi. Its secret is random: keep it, as each call to the private API needs it with the client ID:
API_SECRET=$(openssl rand -base64 32)
curl -u "$API_AUTH" -H "Content-Type: application/json" \
-X POST "$API_URL/api/apikeys" --data @- <<EOF
{
"clientId": "private-api-client",
"clientSecret": "$API_SECRET",
"clientName": "private-api-client",
"authorizations": [{ "kind": "route", "id": "route_private_api" }],
"throttlingQuota": 10
}
EOFYou can also manage these settings from the web interface of Otoroshi, opened with clever otoroshi open webui my-otoroshi.
Test the protections
After a few seconds, Otoroshi serves the route. Without an API key, it rejects the request with a 400 status and the no apikey error:
curl "https://$ROUTES_DOMAIN/api/hello"With the API key, the request reaches the application:
curl -u "private-api-client:$API_SECRET" "https://$ROUTES_DOMAIN/api/hello"{
"path": "/api/hello",
"client": "10.113.0.5",
"headers": {
"host": "app_xxx.m.ng_xxx.cc-ng.cloud",
"accept": "*/*",
"sozu-id": "01M6XZE0PKPGJQM7ZW7NR437M7",
"forwarded": "proto=https;for=\"203.0.113.10:64057\";by=198.51.100.20",
"user-agent": "curl/8.7.1",
"traceparent": "00-84a606f02a267afb013a8094e78a8f74-0febd1452d66a01b-01",
"x-request-id": "01M6XZE0PKPGJQM7ZW7NR437M7",
"x-forwarded-port": "443"
}
}The client of the application is Otoroshi, at its address in the Network Group, and host is the domain name of the member, set by the OverrideHost plugin. The forwarded header keeps the address of the original client. The credentials of the API key don’t reach the application: Otoroshi checks them, and doesn’t forward the authorization header.
Otoroshi only accepts GET requests on this route, and rejects the others with a 405 status:
curl -X POST -u "private-api-client:$API_SECRET" "https://$ROUTES_DOMAIN/api/hello"Beyond the quota, Otoroshi rejects requests with a 429 status until the next window. Send 15 requests in a row: once the quota of the window is reached, the next ones get a 429 status instead of 200:
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code} " -u "private-api-client:$API_SECRET" "https://$ROUTES_DOMAIN/api/hello"
doneOtoroshi identifies a client by its IP address, on every route that uses this plugin. A client that sends a wrong secret 3 times within a minute gets 401 statuses, then a 403 status even with the right secret, until the ban ends:
for i in $(seq 1 3); do
curl -s -o /dev/null -w "%{http_code} " -u "private-api-client:wrong-secret" "https://$ROUTES_DOMAIN/api/hello"
done
curl -u "private-api-client:$API_SECRET" "https://$ROUTES_DOMAIN/api/hello"{"error":"temporary_ban","message":"You are temporarily banned due to too many failed requests.","retry_in_seconds":59}To expose the API on another domain later, add it to its Java application and configure its DNS, then add it to the frontend of the route.
Extend your API
The plugins and extensions of Otoroshi cover many other needs, which this guide doesn’t set up. Some popular ones:
- Keycloak to log your users in, with an authentication module
- JWT tokens issued by your identity provider, checked by Otoroshi
- OAuth 2.0 client credentials, for calls between services
- Eclipse Biscuit tokens, which their holder can restrict, with the Biscuit Studio of Cloud APIM
- A developer portal of Cloud APIM, with the documentation of your API and the keys of its users
- Data exporters, to send the events of the gateway to your analytics tools
- A mailer endpoint of Cloud APIM, which turns your SMTP provider into a REST API to send emails
- A static site, such as the documentation of your API, served from a zip archive
- Workflows, to automate scenarios around your routes, from a visual editor
- Built-in plugins, such as CORS, a response cache, canary releases, or a Swagger UI