From 046fa094d377d7aa5b0308f9e0cd3a1c615c948d Mon Sep 17 00:00:00 2001 From: otoroshi-github-actions Date: Thu, 22 Feb 2024 14:12:18 +0000 Subject: [PATCH] Update dev documentation websites --- docs/devmanual/code/openapi.json | 2 +- docs/devmanual/content-pretty.json | 20 +- docs/devmanual/content.json | 2 +- docs/devmanual/deploy/kubernetes.html | 20 +- docs/devmanual/getting-started.html | 2 +- .../how-to-s/export-events-to-elastic.html | 2 +- .../import-export-otoroshi-datastore.html | 2 +- .../how-to-s/instantiate-waf-coraza.html | 2 +- .../secure-an-app-with-jwt-verifiers.html | 2 +- .../how-to-s/secure-app-with-auth0.html | 2 +- .../how-to-s/secure-app-with-keycloak.html | 2 +- .../how-to-s/secure-app-with-ldap.html | 2 +- ...on-between-a-backend-app-and-otoroshi.html | 2 +- .../how-to-s/secure-with-apikey.html | 2 +- .../how-to-s/secure-with-oauth1-client.html | 2 +- .../how-to-s/setup-otoroshi-cluster.html | 2 +- ...ls-termination-using-own-certificates.html | 2 +- docs/devmanual/how-to-s/wasm-usage.html | 2 +- .../how-to-s/wasmo-installation.html | 8 +- .../how-to-s/working-with-eureka.html | 2 +- .../how-to-s/zip-backend-plugin.html | 2 +- docs/devmanual/index.html | 6 +- docs/devmanual/install/get-otoroshi.html | 10 +- ...15a.pf_fragment => en_10ae743.pf_fragment} | Bin 3001 -> 3001 bytes .../search/fragment/en_12d4cba.pf_fragment | Bin 0 -> 5802 bytes .../search/fragment/en_1a76a97.pf_fragment | Bin 0 -> 3235 bytes .../search/fragment/en_1ca7174.pf_fragment | Bin 0 -> 16804 bytes ...637.pf_fragment => en_23c4b06.pf_fragment} | Bin 1976 -> 1976 bytes .../search/fragment/en_2b3f9c8.pf_fragment | Bin 2189 -> 0 bytes .../search/fragment/en_45cbf27.pf_fragment | Bin 0 -> 2194 bytes .../search/fragment/en_486aa50.pf_fragment | Bin 0 -> 2306 bytes .../search/fragment/en_5f18f35.pf_fragment | Bin 3710 -> 0 bytes ...5cd.pf_fragment => en_631821f.pf_fragment} | Bin 2139 -> 2139 bytes .../search/fragment/en_68727d2.pf_fragment | Bin 3427 -> 0 bytes .../search/fragment/en_6f26169.pf_fragment | Bin 4396 -> 0 bytes .../search/fragment/en_70551fb.pf_fragment | Bin 4549 -> 0 bytes .../search/fragment/en_7cca36a.pf_fragment | Bin 1265 -> 0 bytes .../search/fragment/en_83e2704.pf_fragment | Bin 3060 -> 0 bytes .../search/fragment/en_85ed419.pf_fragment | Bin 0 -> 3427 bytes .../search/fragment/en_88e2b16.pf_fragment | Bin 16804 -> 0 bytes .../search/fragment/en_8d901a7.pf_fragment | Bin 1610 -> 0 bytes .../search/fragment/en_911d366.pf_fragment | Bin 3235 -> 0 bytes .../search/fragment/en_949c56f.pf_fragment | Bin 0 -> 4548 bytes .../search/fragment/en_964c41f.pf_fragment | Bin 1793 -> 0 bytes .../search/fragment/en_9bbb5d8.pf_fragment | Bin 0 -> 1721 bytes .../search/fragment/en_9d5f4c6.pf_fragment | Bin 1721 -> 0 bytes .../search/fragment/en_9e47152.pf_fragment | Bin 0 -> 3711 bytes .../search/fragment/en_a340d04.pf_fragment | Bin 0 -> 3060 bytes .../search/fragment/en_c468bb2.pf_fragment | Bin 3150 -> 0 bytes .../search/fragment/en_c4e54e4.pf_fragment | Bin 0 -> 4394 bytes .../search/fragment/en_c6236ad.pf_fragment | Bin 435 -> 0 bytes .../search/fragment/en_cd35774.pf_fragment | Bin 2196 -> 0 bytes .../search/fragment/en_d323b3f.pf_fragment | Bin 2307 -> 0 bytes .../search/fragment/en_dbcf709.pf_fragment | Bin 0 -> 435 bytes ...d08.pf_fragment => en_ddf2b83.pf_fragment} | Bin 3028 -> 3028 bytes .../search/fragment/en_e42b384.pf_fragment | Bin 0 -> 1793 bytes .../search/fragment/en_efa0481.pf_fragment | Bin 0 -> 2189 bytes .../search/fragment/en_f098ba1.pf_fragment | Bin 5803 -> 0 bytes .../search/fragment/en_f15936d.pf_fragment | Bin 0 -> 1610 bytes .../search/fragment/en_f3a924a.pf_fragment | Bin 0 -> 1265 bytes .../search/fragment/en_f6ae855.pf_fragment | Bin 0 -> 3149 bytes .../search/index/en_13565fd.pf_index | Bin 41514 -> 0 bytes .../search/index/en_17b33de.pf_index | Bin 0 -> 46678 bytes .../search/index/en_31ee9fc.pf_index | Bin 40456 -> 0 bytes .../search/index/en_418e556.pf_index | Bin 46662 -> 0 bytes .../search/index/en_49d892f.pf_index | Bin 43098 -> 0 bytes .../search/index/en_4a479d4.pf_index | Bin 40137 -> 0 bytes .../search/index/en_5660ea1.pf_index | Bin 43297 -> 0 bytes .../search/index/en_56e5732.pf_index | Bin 0 -> 41499 bytes .../search/index/en_624647d.pf_index | Bin 2789 -> 0 bytes .../search/index/en_68fd426.pf_index | Bin 0 -> 40128 bytes .../search/index/en_7053ee2.pf_index | Bin 0 -> 43675 bytes .../search/index/en_7c7961f.pf_index | Bin 0 -> 40492 bytes .../search/index/en_8365865.pf_index | Bin 40922 -> 0 bytes .../search/index/en_a5f29ea.pf_index | Bin 0 -> 2802 bytes .../search/index/en_ade7e04.pf_index | Bin 0 -> 40907 bytes .../search/index/en_ae3d1a7.pf_index | Bin 0 -> 40404 bytes .../search/index/en_b0eaf31.pf_index | Bin 0 -> 43102 bytes .../search/index/en_b2a4f87.pf_index | Bin 43699 -> 0 bytes .../search/index/en_bdf10a3.pf_index | Bin 0 -> 41858 bytes .../search/index/en_c919a42.pf_index | Bin 40500 -> 0 bytes .../search/index/en_c9749dd.pf_index | Bin 0 -> 43260 bytes .../search/index/en_f57a6cc.pf_index | Bin 41866 -> 0 bytes docs/devmanual/search/pagefind-entry.json | 2 +- .../search/pagefind.en_c66a8cf98e.pf_meta | Bin 0 -> 927 bytes .../search/pagefind.en_eeec74b634.pf_meta | Bin 931 -> 0 bytes docs/devmanual/snippets/build.gradle | 4 +- docs/devmanual/snippets/build.sbt | 4 +- docs/devmanual/snippets/fetch.sh | 4 +- .../devmanual/topics/expression-language.html | 2 +- manual/src/main/paradox/content-pretty.json | 20 +- manual/src/main/paradox/content.json | 2 +- .../main/paradox/snippets/reference-env.conf | 1056 ---------- .../src/main/paradox/snippets/reference.conf | 1712 ----------------- 94 files changed, 69 insertions(+), 2837 deletions(-) rename docs/devmanual/search/fragment/{en_227b15a.pf_fragment => en_10ae743.pf_fragment} (96%) create mode 100644 docs/devmanual/search/fragment/en_12d4cba.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_1a76a97.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_1ca7174.pf_fragment rename docs/devmanual/search/fragment/{en_ab37637.pf_fragment => en_23c4b06.pf_fragment} (94%) delete mode 100644 docs/devmanual/search/fragment/en_2b3f9c8.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_45cbf27.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_486aa50.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_5f18f35.pf_fragment rename docs/devmanual/search/fragment/{en_ca525cd.pf_fragment => en_631821f.pf_fragment} (94%) delete mode 100644 docs/devmanual/search/fragment/en_68727d2.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_6f26169.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_70551fb.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_7cca36a.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_83e2704.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_85ed419.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_88e2b16.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_8d901a7.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_911d366.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_949c56f.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_964c41f.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_9bbb5d8.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_9d5f4c6.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_9e47152.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_a340d04.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_c468bb2.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_c4e54e4.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_c6236ad.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_cd35774.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_d323b3f.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_dbcf709.pf_fragment rename docs/devmanual/search/fragment/{en_9c36d08.pf_fragment => en_ddf2b83.pf_fragment} (97%) create mode 100644 docs/devmanual/search/fragment/en_e42b384.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_efa0481.pf_fragment delete mode 100644 docs/devmanual/search/fragment/en_f098ba1.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_f15936d.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_f3a924a.pf_fragment create mode 100644 docs/devmanual/search/fragment/en_f6ae855.pf_fragment delete mode 100644 docs/devmanual/search/index/en_13565fd.pf_index create mode 100644 docs/devmanual/search/index/en_17b33de.pf_index delete mode 100644 docs/devmanual/search/index/en_31ee9fc.pf_index delete mode 100644 docs/devmanual/search/index/en_418e556.pf_index delete mode 100644 docs/devmanual/search/index/en_49d892f.pf_index delete mode 100644 docs/devmanual/search/index/en_4a479d4.pf_index delete mode 100644 docs/devmanual/search/index/en_5660ea1.pf_index create mode 100644 docs/devmanual/search/index/en_56e5732.pf_index delete mode 100644 docs/devmanual/search/index/en_624647d.pf_index create mode 100644 docs/devmanual/search/index/en_68fd426.pf_index create mode 100644 docs/devmanual/search/index/en_7053ee2.pf_index create mode 100644 docs/devmanual/search/index/en_7c7961f.pf_index delete mode 100644 docs/devmanual/search/index/en_8365865.pf_index create mode 100644 docs/devmanual/search/index/en_a5f29ea.pf_index create mode 100644 docs/devmanual/search/index/en_ade7e04.pf_index create mode 100644 docs/devmanual/search/index/en_ae3d1a7.pf_index create mode 100644 docs/devmanual/search/index/en_b0eaf31.pf_index delete mode 100644 docs/devmanual/search/index/en_b2a4f87.pf_index create mode 100644 docs/devmanual/search/index/en_bdf10a3.pf_index delete mode 100644 docs/devmanual/search/index/en_c919a42.pf_index create mode 100644 docs/devmanual/search/index/en_c9749dd.pf_index delete mode 100644 docs/devmanual/search/index/en_f57a6cc.pf_index create mode 100644 docs/devmanual/search/pagefind.en_c66a8cf98e.pf_meta delete mode 100644 docs/devmanual/search/pagefind.en_eeec74b634.pf_meta diff --git a/docs/devmanual/code/openapi.json b/docs/devmanual/code/openapi.json index 87307eb308..e4bbe04977 100644 --- a/docs/devmanual/code/openapi.json +++ b/docs/devmanual/code/openapi.json @@ -3,7 +3,7 @@ "info" : { "title" : "Otoroshi Admin API", "description" : "Admin API of the Otoroshi reverse proxy", - "version" : "16.14.0-dev", + "version" : "16.15.0-dev", "contact" : { "name" : "Otoroshi Team", "email" : "oss@maif.fr" diff --git a/docs/devmanual/content-pretty.json b/docs/devmanual/content-pretty.json index 1b7723009c..2d86510ed3 100644 --- a/docs/devmanual/content-pretty.json +++ b/docs/devmanual/content-pretty.json @@ -53,7 +53,7 @@ "id": "/deploy/kubernetes.md", "url": "/deploy/kubernetes.html", "title": "Kubernetes", - "content": "# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.14.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.14.0-dev\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n" + "content": "# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.15.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.15.0-dev\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n" }, { "name": "scaling.md", @@ -186,7 +186,7 @@ "id": "/getting-started.md", "url": "/getting-started.html", "title": "Getting Started", - "content": "# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
\nRoute plugins:\nApikeys\n
\n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
\nRoute plugins:\nAuthentication\n
\n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial." + "content": "# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
\nRoute plugins:\nApikeys\n
\n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
\nRoute plugins:\nAuthentication\n
\n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial." }, { "name": "communicate-with-kafka.md", @@ -242,7 +242,7 @@ "id": "/how-to-s/import-export-otoroshi-datastore.md", "url": "/how-to-s/import-export-otoroshi-datastore.html", "title": "Import and export Otoroshi datastore", - "content": "# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`" + "content": "# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`" }, { "name": "index.md", @@ -326,7 +326,7 @@ "id": "/how-to-s/setup-otoroshi-cluster.md", "url": "/how-to-s/setup-otoroshi-cluster.html", "title": "Setup an Otoroshi cluster", - "content": "# Setup an Otoroshi cluster\n\n
\nRoute plugins:\nAdditional Headers In\n
\n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n" + "content": "# Setup an Otoroshi cluster\n\n
\nRoute plugins:\nAdditional Headers In\n
\n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n" }, { "name": "tailscale-integration.md", @@ -361,7 +361,7 @@ "id": "/how-to-s/wasmo-installation.md", "url": "/how-to-s/wasmo-installation.html", "title": "Deploy your own Wasmo", - "content": "# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: admin-api-apikey-id\n* `Apikey id`: admin-api-apikey-secret\n* `Apikey secret`: http://localhost:5001\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n" + "content": "# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: http://localhost:5001\n* `Apikey id`: admin-api-apikey-id\n* `Apikey secret`: admin-api-apikey-secret\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n" }, { "name": "working-with-eureka.md", @@ -396,28 +396,28 @@ "id": "/includes/fetch-and-start.md", "url": "/includes/fetch-and-start.html", "title": "", - "content": "\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n" + "content": "\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n" }, { "name": "initialize.md", "id": "/includes/initialize.md", "url": "/includes/initialize.html", "title": "", - "content": "\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
\nSet up an Otoroshi\n\n
\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n" + "content": "\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
\nSet up an Otoroshi\n\n
\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n" }, { "name": "index.md", "id": "/index.md", "url": "/index.html", "title": "Otoroshi", - "content": "# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.14.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n" + "content": "# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.15.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n" }, { "name": "get-otoroshi.md", "id": "/install/get-otoroshi.md", "url": "/install/get-otoroshi.html", "title": "Get Otoroshi", - "content": "# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi-16.14.0-dev.zip\nunzip ./otoroshi-16.14.0-dev.zip\ncd otoroshi-16.14.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.14.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n" + "content": "# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi-16.15.0-dev.zip\nunzip ./otoroshi-16.15.0-dev.zip\ncd otoroshi-16.15.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.15.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n" }, { "name": "index.md", @@ -522,7 +522,7 @@ "id": "/topics/expression-language.md", "url": "/topics/expression-language.html", "title": "Expression language", - "content": "# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```" + "content": "# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```" }, { "name": "graphql-composer.md", diff --git a/docs/devmanual/content.json b/docs/devmanual/content.json index bed452bfbd..eb24510122 100644 --- a/docs/devmanual/content.json +++ b/docs/devmanual/content.json @@ -1 +1 @@ -[{"name":"about.md","id":"/about.md","url":"/about.html","title":"About Otoroshi","content":"# About Otoroshi\n\nAt the beginning of 2017, we had the need to create a new environment to be able to create new \"digital\" products very quickly in an agile fashion at @link:[MAIF](https://www.maif.fr) { open=new }. Naturally we turned to PaaS solutions and chose the excellent @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } product to run our apps. \n\nWe also chose that every feature team will have the freedom to choose its own technological stack to build its product. It was a nice move but it has also introduced some challenges in terms of homogeneity for traceability, security, logging, ... because we did not want to force library usage in the products. We could have used something like @link:[Service Mesh Pattern](http://philcalcado.com/2017/08/03/pattern_service_mesh.html) { open=new } but the deployement model of @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } prevented us to do it.\n\nThe right solution was to use a reverse proxy or some kind of API Gateway able to provide tracability, logging, security with apikeys, quotas, DNS as a service locator, etc. We needed something easy to use, with a human friendly UI, a nice API to extends its features, true hot reconfiguration, able to generate internal events for third party usage. A couple of solutions were available at that time, but not one seems to fit our needs, there was always something missing, too complicated for our needs or not playing well with @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } deployment model.\n\nAt some point, we tried to write a small prototype to explore what could be our dream reverse proxy. The design was very simple, there were some rough edges but every major feature needed was there waiting to be enhanced.\n\n**Otoroshi** was born and we decided to move ahead with our hairy monster :)\n\n## Philosophy \n\nEvery OSS product build at @link:[MAIF](https://www.maif.fr) { open=new } like the developer portal @link:[Daikoku](https://maif.github.io/daikoku) { open=new } or @link:[Izanami](https://maif.github.io/izanami) { open=new } follow a common philosophy. \n\n* the services or API provided should be **technology agnostic**.\n* **http first**: http is the right answer to the previous quote \n* **api First**: the UI is just another client of the api. \n* **secured**: the services exposed need authentication for both humans or machines \n* **event based**: the services should expose a way to get notified of what happened inside. \n"},{"name":"api.md","id":"/api.md","url":"/api.html","title":"Admin REST API","content":"# Admin REST API\n\nOtoroshi provides a fully featured REST admin API to perform almost every operation possible in the Otoroshi dashboard. The Otoroshi dashbaord is just a regular consumer of the admin API.\n\nUsing the admin API, you can do whatever you want and enhance your Otoroshi instances with a lot of features that will feet your needs.\n\n## Swagger descriptor\n\nThe Otoroshi admin API is described using OpenAPI format and is available at :\n\nhttps://maif.github.io/otoroshi/manual/code/openapi.json\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/openapi.json\n\n## Swagger documentation\n\nYou can read the OpenAPI descriptor in a more human friendly fashion using `Swagger UI`. The swagger UI documentation of the Otoroshi admin API is available at :\n\nhttps://maif.github.io/otoroshi/swagger-ui/index.html\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/swagger/ui\n\nYou can also read the swagger UI documentation of the Otoroshi admin API below :\n\n@@@ div { .swagger-frame }\n\n\n@@@\n"},{"name":"architecture.md","id":"/architecture.md","url":"/architecture.html","title":"Architecture","content":"# Architecture\n\nWhen we started the development of Otoroshi, we had several classical patterns in mind like `Service gateway`, `Service locator`, `Circuit breakers`, etc ...\n\nAt start we thought about providing a bunch of librairies that would be included in each microservice or app to perform these tasks. But the more we were thinking about it, the more it was feeling weird, unagile, etc, it also prevented us to use any technical stack we wanted to use. So we decided to change our approach to something more universal.\n\nWe chose to make Otoroshi the central part of our microservices system, something between a reverse-proxy, a service gateway and a service locator where each call to a microservice (even from another microservice) must pass through Otoroshi. There are multiple benefits to do that, each call can be logged, audited, monitored, integrated with a circuit breaker, etc without imposing libraries and technical stack. Any service is exposed through its own domain and we rely only on DNS to handle the service location part. Any access to a service is secured by default with an api key and is supervised by a circuit breaker to avoid cascading failures.\n\n@@@ div { .centered-img }\n\n@@@\n\nOtoroshi tries to embrace our @ref:[global philosophy](./about.md#philosophy) by providing a full featured REST admin api, a gorgeous admin dashboard written in @link:[React](https://reactjs.org) { open=new } that uses the api, by generating traffic events, alerts events, audit events that can be consumed by several channels. Otoroshi also supports a bunch of datastores to better match with different use cases.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"aws.md","id":"/deploy/aws.md","url":"/deploy/aws.html","title":"AWS - Elastic Beanstalk","content":"# AWS - Elastic Beanstalk\n\nNow you want to use Otoroshi on AWS. There are multiple options to deploy Otoroshi on AWS, \nfor instance :\n\n* You can deploy the @ref:[Docker image](../install/get-otoroshi.md#from-docker) on [Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n* You can create a basic [Amazon EC2](https://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/concepts.html), access it via SSH, then \ndeploy the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file) \n* Or you can use [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk)\n\nIn this section we are going to cover how to deploy Otoroshi on [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk). \n\n## AWS Elastic Beanstalk Overview\nUnlike Clever Cloud, to deploy an application on AWS Elastic Beanstalk, you don't link your app to your VCS repository, push your code and expect it to be built and run.\n\nAWS Elastic Beanstalk does only the run part. So you have to handle your own build pipeline, upload a Zip file containing your runnable, then AWS Elastic Beanstalk will take it from there. \n \nEg: for apps running on the JVM (Scala/Java/Kotlin) a Zip with the jar inside would suffice, for apps running in a Docker container, a Zip with the DockerFile would be enough. \n\n\n## Prepare your deployment target\nActually, there are 2 options to build your target. \n\nEither you create a DockerFile from this @ref:[Docker image](../install/get-otoroshi.md#from-docker), build a zip, and do all the Otoroshi custom configuration using ENVs.\n\nOr you download the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file), do all the Otoroshi custom configuration using your own otoroshi.conf, and create a DockerFile that runs the jar using your otoroshi.conf. \n\nFor the second option your DockerFile would look like this :\n\n```dockerfile\nFROM openjdk:11\nVOLUME /tmp\nEXPOSE 8080\nADD otoroshi.jar otoroshi.jar\nADD otoroshi.conf otoroshi.conf\nRUN sh -c 'touch /otoroshi.jar'\nENV JAVA_OPTS=\"\"\nENTRYPOINT [ \"sh\", \"-c\", \"java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dconfig.file=/otoroshi.conf -jar /otoroshi.jar\" ]\n``` \n \nI'd recommend the second option.\n \nNow Zip your target (Jar + Conf + DockerFile) and get ready for deployment. \n\n## Create an Otoroshi instance on AWS Elastic Beanstalk\nFirst, go to [AWS Elastic Beanstalk Console](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/welcome), don't forget to sign in and make sure that you are in the good region (eg : eu-west-3 for Paris).\n\nHit **Get started** \n\n@@@ div { .centered-img }\n\n@@@\n\nSpecify the **Application name** of your application, Otoroshi for example.\n\n@@@ div { .centered-img }\n\n@@@\n \nChoose the **Platform** of the application you want to create, in your case use Docker.\n\nFor **Application code** choose **Upload your code** then hit **Upload**.\n\n@@@ div { .centered-img }\n\n@@@\n\nBrowse the zip created in the [previous section](#prepare-your-deployment-target) from your machine. \n\nAs you can see in the image above, you can also choose an S3 location, you can imagine that at the end of your build pipeline you upload your Zip to S3, and then get it from there (I wouldn't recommend that though).\n \nWhen the upload is done, hit **Configure more options**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nRight now an AWS Elastic Beanstalk application has been created, and by default an environment named Otoroshi-env is being created as well.\n\nAWS Elastic Beanstalk can manage multiple environments of the same application, for instance environments can be (prod, preprod, expriments...). \n\nOtoroshi is a bit particular, it doesn't make much sense to have multiple environments, since Otoroshi will handle all the requests from/to backend services regardless of the environment. \n \nAs you see in the image above, we are now configuring the Otoroshi-env, the one and only environment of Otoroshi.\n \nFor **Configuration presets**, choose custom configuration, now you have a load balancer for your environment with the capacity of at least one instance and at most four.\nI'd recommend at least 2 instances, to change that, on the **Capacity** card hit **Modify**. \n\n@@@ div { .centered-img }\n\n@@@\n\nChange the **Instances** to min 2, max 4 then hit **Save**. For the **Scaling triggers**, I'd keep the default values, but know that you can edit the capacity config any time you want, it only costs a redeploy, which will be done automatically by the way.\n \nInstances size is by default t2.micro, which is a bit small for running Otoroshi, I'd recommend a t2.medium. \nOn the **Instances** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFor **Instance type** choose t2.medium, then hit **Save**, no need to change the volume size, unless you have a lot of http call faults, which means a lot more logs, in that case the default volume size may not be enough.\n\nThe default environment created for Otoroshi, for instance Otoroshi-env, is a web server environment which fits in your case, but the thing is that on AWS Elastic Beanstalk by default a web server environment for a docker-based application, runs behind an Nginx proxy.\nWe have to remove that proxy. So on the **Software** card hit **Modify**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nFor **Proxy server** choose None then hit **Save**.\n\nAlso note that you can set Envs for Otoroshi in same page (see image below). \n\n@@@ div { .centered-img }\n\n@@@ \n\nTo finalise the creation process, hit **Create app** on the bottom right.\n\nThe Otoroshi app is now created, and it's running which is cool, but we still don't have neither a **datastore** nor **https**.\n \n## Create an Otoroshi datastore on AWS ElastiCache\n\nBy default Otoroshi uses non persistent memory to store it's data, Otoroshi supports many kinds of datastores. In this section we will be covering Redis datastore. \n\nBefore starting, using a datastore hosted by AWS is not at all mandatory, feel free to use your own if you like, but if you want to learn more about ElastiCache, this section may interest you, otherwise you can skip it.\n\nGo to [AWS ElastiCache](https://eu-west-3.console.aws.amazon.com/elasticache/home?region=eu-west-3#) and hit **Get Started Now**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nFor **Cluster engine** keep Redis.\n\nChoose a **Name** for your datastore, for instance otoroshi-datastore.\n\nYou can keep all the other default values and hit **Create** on the bottom right of the page.\n\nOnce your Redis Cluster is created, it would look like the image below.\n\n@@@ div { .centered-img }\n\n@@@ \n\n\nFor applications in the same security group as your cluster, redis cluster is accessible via the **Primary Endpoint**. Don't worry the default security group is fine, you don't need any configuration to access the cluster from Otoroshi.\n\nTo make Otoroshi use the created cluster, you can either use Envs `APP_STORAGE=redis`, `REDIS_HOST` and `REDIS_PORT`, or set `otoroshi.storage=redis`, `otoroshi.redis.host` and `otoroshi.redis.port` in your otoroshi.conf.\n\n## Create SSL certificate and configure your domain\n\nOtoroshi has now a datastore, but not yet ready for use. \n\nIn order to get it ready you need to :\n\n* Configure Otoroshi with your domain \n* Create a wildcard SSL certificate for your domain\n* Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n* Configure your DNS to redirect all traffic on your domain to Otoroshi \n \n### Configure Otoroshi with your domain\n\nYou can use ENVs or you can use a custom otoroshi.conf in your Docker container.\n\nFor the second option your otoroshi.conf would look like this :\n\n``` \n include \"application.conf\"\n http.port = 8080\n app {\n env = \"prod\"\n domain = \"mysubdomain.oto.tools\"\n rootScheme = \"https\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n \n storage = \"redis\"\n redis {\n host=\"myredishost\"\n port=myredisport\n }\n \n privateapps {\n subdomain = \"privateapps\"\n }\n \n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-client-id\"\n backOfficeApiKeyClientSecret = \"admin-client-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n proxy {\n https = true\n local = false\n }\n }\n claim {\n sharedKey = \"myclaimsharedkey\"\n }\n }\n \n play.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2147483646\n domain = \".mysubdomain.oto.tools\"\n cookieName = \"oto-sess\"\n }\n }\n``` \n\n### Create a wildcard SSL certificate for your domain\n\nGo to [AWS Certificate Manager](https://eu-west-3.console.aws.amazon.com/acm/home?region=eu-west-3#/firstrun).\n\nBelow **Provision certificates** hit **Get started**.\n\n@@@ div { .centered-img }\n\n@@@ \n \nKeep the default selected value **Request a public certificate** and hit **Request a certificate**.\n \n@@@ div { .centered-img }\n\n@@@ \n\nPut your **Domain name**, use *. for wildcard, for instance *\\*.mysubdomain.oto.tools*, then hit **Next**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nYou can choose between **Email validation** and **DNS validation**, I'd recommend **DNS validation**, then hit **Review**. \n \n@@@ div { .centered-img }\n\n@@@ \n \nVerify that you did put the right **Domain name** then hit **Confirm and request**. \n\n@@@ div { .centered-img }\n\n@@@\n \nAs you see in the image above, to let Amazon do the validation you have to add the `CNAME` record to your DNS configuration. Normally this operation takes around one day.\n \n### Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n\nOnce the certificate is validated, you need to modify the configuration of Otoroshi-env to add the SSL certificate for HTTPS. \nFor that you need to go to [AWS Elastic Beanstalk applications](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/applications),\nhit **Otoroshi-env**, then on the left side hit **Configuration**, then on the **Load balancer** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nIn the **Application Load Balancer** section hit **Add listener**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFill the popup as the image above, then hit **Add**. \n\nYou should now be seeing something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n \n \nMake sure that your listener is enabled, and on the bottom right of the page hit **Apply**.\n\nNow you have **https**, so let's use Otoroshi.\n\n### Configure your DNS to redirect all traffic on your domain to Otoroshi\n \nIt's actually pretty simple, you just need to add a `CNAME` record to your DNS configuration, that redirects *\\*.mysubdomain.oto.tools* to the DNS name of Otoroshi's load balancer.\n\nTo find the DNS name of Otoroshi's load balancer go to [AWS Ec2](https://eu-west-3.console.aws.amazon.com/ec2/v2/home?region=eu-west-3#LoadBalancers:tag:elasticbeanstalk:environment-name=Otoroshi-env;sort=loadBalancerName)\n\nYou would find something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n\nThere is your DNS name, so add your `CNAME` record. \n \nOnce all these steps are done, the AWS Elastic Beanstalk Otoroshi instance, would now be handling all the requests on your domain. ;) \n"},{"name":"clever-cloud.md","id":"/deploy/clever-cloud.md","url":"/deploy/clever-cloud.html","title":"Clever-Cloud","content":"# Clever-Cloud\n\nNow you want to use Otoroshi on Clever Cloud. Otoroshi has been designed and created to run on Clever Cloud and a lot of choices were made because of how Clever Cloud works.\n\n## Create an Otoroshi instance on CleverCloud\n\nIf you want to customize the configuration @ref:[use env. variables](../install/setup-otoroshi.md#configuration-with-env-variables), you can use [the example provided below](#example-of-clevercloud-env-variables)\n\nCreate a new CleverCloud app based on a clevercloud git repo (not empty) or a github project of your own (not empty).\n\n@@@ div { .centered-img }\n\n@@@\n\nThen choose what kind of app your want to create, for Otoroshi, choose `Java + Jar`\n\n@@@ div { .centered-img }\n\n@@@\n\nNext, set up choose instance size and auto-scalling. Otoroshi can run on small instances, especially if you just want to test it.\n\n@@@ div { .centered-img }\n\n@@@\n\nFinally, choose a name for your app\n\n@@@ div { .centered-img }\n\n@@@\n\nNow you just need to customize environnment variables\n\nat this point, you can also add other env. variables to configure Otoroshi like in [the example provided below](#example-of-clevercloud-env-variables)\n\n@@@ div { .centered-img }\n\n@@@\n\nYou can also use expert mode :\n\n@@@ div { .centered-img }\n\n@@@\n\nNow, your app is ready, don't forget to add a custom domains name on the CleverCloud app matching the Otoroshi app domain. \n\n## Example of CleverCloud env. variables\n\nYou can add more env variables to customize your Otoroshi instance like the following. Use the expert mode to copy/paste all the values in one shot. If you want an real datastore, create a redis addon on clevercloud, link it to your otoroshi app and change the `APP_STORAGE` variable to `redis`\n\n
\n\n
\n```\nADMIN_API_CLIENT_ID=xxxx\nADMIN_API_CLIENT_SECRET=xxxxx\nADMIN_API_GROUP=xxxxxx\nADMIN_API_SERVICE_ID=xxxxxxx\nCLAIM_SHAREDKEY=xxxxxxx\nOTOROSHI_INITIAL_ADMIN_LOGIN=youremailaddress\nOTOROSHI_INITIAL_ADMIN_PASSWORD=yourpassword\nPLAY_CRYPTO_SECRET=xxxxxx\nSESSION_NAME=oto-session\nAPP_DOMAIN=yourdomain.tech\nAPP_ENV=prod\nAPP_STORAGE=inmemory\nAPP_ROOT_SCHEME=https\nCC_PRE_BUILD_HOOK=curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/${latest_otoroshi_version}/otoroshi.jar'\nCC_JAR_PATH=./otoroshi.jar\nCC_JAVA_VERSION=11\nPORT=8080\nSESSION_DOMAIN=.yourdomain.tech\nSESSION_MAX_AGE=604800000\nSESSION_SECURE_ONLY=true\nUSER_AGENT=otoroshi\nMAX_EVENTS_SIZE=1\nWEBHOOK_SIZE=100\nAPP_BACKOFFICE_SESSION_EXP=86400000\nAPP_PRIVATEAPPS_SESSION_EXP=86400000\nENABLE_METRICS=true\nOTOROSHI_ANALYTICS_PRESSURE_ENABLED=true\nUSE_CACHE=true\n```\n
"},{"name":"clustering.md","id":"/deploy/clustering.md","url":"/deploy/clustering.html","title":"Otoroshi clustering","content":"# Otoroshi clustering\n\nOtoroshi can work as a cluster by default as you can spin many Otoroshi servers using the same datastore or datastore cluster. In that case any instance is capable of serving services, Otoroshi admin UI, Otoroshi admin API, etc.\n\nBut sometimes, this is not enough. So Otoroshi provides an additional clustering model named `Leader / Workers` where there is a leader cluster ([control plane](https://en.wikipedia.org/wiki/Control_plane)), composed of Otoroshi instances backed by a datastore like Redis, PostgreSQL or Cassandra, that is in charge of all `writes` to the datastore through Otoroshi admin UI and API, and a worker cluster ([data plane](https://en.wikipedia.org/wiki/Forwarding_plane)) composed of horizontally scalable Otoroshi instances, backed by a super fast in memory datastore, with the sole purpose of routing traffic to your services based on data synced from the leader cluster. With this distributed Otoroshi version, you can reach your goals of high availability, scalability and security.\n\nOtoroshi clustering only uses http internally (right now) to make communications between leaders and workers instances so it is fully compatible with PaaS providers like [Clever-Cloud](https://www.clever-cloud.com/en/) that only provide one external port for http traffic.\n\n@@@ div { .centered-img }\n\n\n*Fig. 1: Simplified view*\n@@@\n\n@@@ div { .centered-img }\n\n\n*Fig. 2: Deployment view*\n@@@\n\n## Cluster configuration\n\n```hocon\notoroshi {\n cluster {\n mode = \"leader\" # can be \"off\", \"leader\", \"worker\"\n compression = 4 # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9\n leader {\n name = ${?CLUSTER_LEADER_NAME} # name of the instance, if none, it will be generated\n urls = [\"http://127.0.0.1:8080\"] # urls to contact the leader cluster\n host = \"otoroshi-api.oto.tools\" # host of the otoroshi api in the leader cluster\n clientId = \"apikey-id\" # otoroshi api client id\n clientSecret = \"secret\" # otoroshi api client secret\n cacheStateFor = 4000 # state is cached during (ms)\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME} # name of the instance, if none, it will be generated\n retries = 3 # number of retries when calling leader cluster\n timeout = 2000 # timeout when calling leader cluster\n state {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on state sync\n pollEvery = 10000 # interval of time (ms) between 2 state sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on state sync\n }\n quotas {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on quotas sync\n pushEvery = 2000 # interval of time (ms) between 2 quotas sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on quotas sync\n }\n }\n }\n}\n```\n\nyou can also use many env. variables to configure Otoroshi cluster\n\n```hocon\notoroshi {\n cluster {\n mode = ${?CLUSTER_MODE}\n compression = ${?CLUSTER_COMPRESSION}\n leader {\n name = ${?CLUSTER_LEADER_NAME}\n host = ${?CLUSTER_LEADER_HOST}\n url = ${?CLUSTER_LEADER_URL}\n clientId = ${?CLUSTER_LEADER_CLIENT_ID}\n clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET}\n groupingBy = ${?CLUSTER_LEADER_GROUP_BY}\n cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR}\n stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH}\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME}\n retries = ${?CLUSTER_WORKER_RETRIES}\n timeout = ${?CLUSTER_WORKER_TIMEOUT}\n state {\n retries = ${?CLUSTER_WORKER_STATE_RETRIES}\n pollEvery = ${?CLUSTER_WORKER_POLL_EVERY}\n timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT}\n }\n quotas {\n retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES}\n pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY}\n timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT}\n }\n }\n }\n}\n```\n\n@@@ warning\nYou **should** use HTTPS exposition for the Otoroshi API that will be used for data sync as sensitive informations are exchanged between control plane and data plane.\n@@@\n\n@@@ warning\nYou **must** have the same cluster configuration on every Otoroshi instance (worker/leader) with only names and mode changed for each instance. Some things in leader/worker are computed using configuration of their counterpart worker/leader.\n@@@\n\n## Cluster UI\n\nOnce an Otoroshi instance is launcher as cluster Leader, a new row of live metrics tile will be available on the home page of Otoroshi admin UI.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can also access a more detailed view of the cluster at `Settings (cog icon) / Cluster View`\n\n@@@ div { .centered-img }\n\n@@@\n\n## Run examples\n\nfor leader \n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nfor worker\n\n```sh\njava -Dhttp.port=8092 -Dhttps.port=9092 -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0=http://127.0.0.1:8091 -jar otoroshi.jar\n```\n\n## Setup a cluster by example\n\nif you want to see how to setup an otoroshi cluster, just check @ref:[the clustering tutorial](../how-to-s/setup-otoroshi-cluster.md)"},{"name":"index.md","id":"/deploy/index.md","url":"/deploy/index.html","title":"Deploy to production","content":"# Deploy to production\n\nNow it's time to deploy Otoroshi in production, in this chapter we will see what kind of things you can do.\n\nOtoroshi can run wherever you want, even on a raspberry pi (Cluster^^) ;)\n\n@@@div { .plugin .platform }\n\n## Cloud APIM\n\nCloud APIM provides Otoroshi instances as a service. You can easily create production ready Otoroshi clusters in just a few clics.\n\n\n[Documentation](https://www.cloud-apim.com/)\n@@@\n\n@@@div { .plugin .platform }\n\n## Clever Cloud\n\nOtoroshi provides an integration to create easily services based on application deployed on your Clever Cloud account.\n\n\n@ref:[Documentation](./clever-cloud.md)\n@@@\n\n@@@div { .plugin .platform } \n## Kubernetes\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support.\n\n\n\n@ref:[Documentation](./kubernetes.md)\n@@@\n\n@@@div { .plugin .platform } \n## AWS Elastic Beanstalk\n\nRun Otoroshi on AWS Elastic Beanstalk\n\n\n\n@ref:[Tutorial](./aws.md)\n@@@\n\n@@@div { .plugin .platform } \n## Amazon ECS\n\nDeploy the Otoroshi Docker image using Amazon Elastic Container Service\n\n\n\n@link:[Tutorial](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform }\n## GCE\n\nDeploy the Docker image using Google Compute Engine container integration\n\n\n\n@link:[Documentation](https://cloud.google.com/compute/docs/containers/deploying-containers)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform } \n## Azure\n\nDeploy the Docker image using Azure Container Service\n\n\n\n@link:[Documentation](https://azure.microsoft.com/en-us/services/container-service/)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker) \n@@@\n\n@@@div { .plugin .platform } \n## Heroku\n\nDeploy the Docker image using Docker integration\n\n\n\n@link:[Documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform } \n## CloudFoundry\n\nDeploy the Docker image using -Docker integration\n\n\n\n@link:[Documentation](https://docs.cloudfoundry.org/adminguide/docker.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform .platform-actions-column } \n## Your own infrastructure\n\nAs Otoroshi is a Play Framework application, you can read the doc about putting a `Play` app in production.\n\nDownload the latest Otoroshi distribution, unzip it, customize it and run it.\n\n@link:[Play Framework](https://www.playframework.com)\n@link:[Production Configuration](https://www.playframework.com/documentation/2.6.x/ProductionConfiguration)\n@ref:[Otoroshi distribution](../install/get-otoroshi.md#from-zip)\n@@@\n\n@@@div { .break }\n## Scaling and clustering in production\n@@@\n\n\n@@@div { .plugin .platform .dark-platform } \n## Clustering\n\nDeploy Otoroshi as a cluster of leaders and workers.\n\n\n@ref:[Documentation](./clustering.md)\n@@@\n\n@@@div { .plugin .platform .dark-platform } \n## Scaling Otoroshi\n\nOtoroshi is designed to be reasonably easy to scale and be highly available.\n\n\n@ref:[Documentation](./scaling.md) \n@@@\n\n@@@ index\n\n* [Clustering](./clustering.md)\n* [Kubernetes](./kubernetes.md)\n* [Clever Cloud](./clever-cloud.md)\n* [AWS - Elastic Beanstalk](./aws.md)\n* [Scaling](./scaling.md) \n\n@@@\n"},{"name":"kubernetes.md","id":"/deploy/kubernetes.md","url":"/deploy/kubernetes.html","title":"Kubernetes","content":"# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.14.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.14.0-dev\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"},{"name":"scaling.md","id":"/deploy/scaling.md","url":"/deploy/scaling.html","title":"Scaling Otoroshi","content":"# Scaling Otoroshi\n\n## Using multiple instances with a front load balancer\n\nOtoroshi has been designed to work with multiple instances. If you already have an infrastructure using frontal load balancing, you just have to declare Otoroshi instances as the target of all domain names handled by Otoroshi\n\n## Using master / workers mode of Otoroshi\n\nYou can read everything about it in @ref:[the clustering section](../deploy/clustering.md) of the documentation.\n\n## Using IPVS\n\nYou can use [IPVS](https://en.wikipedia.org/wiki/IP_Virtual_Server) to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi. You can find example of configuration [here](http://www.linuxvirtualserver.org/VS-DRouting.html) \n\n## Using DNS Round Robin\n\nYou can use [DNS round robin technique](https://en.wikipedia.org/wiki/Round-robin_DNS) to declare multiple A records under the domain names handled by Otoroshi.\n\n## Using software L4/L7 load balancers\n\nYou can use software L4 load balancers like NGINX or HAProxy to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi.\n\nNGINX L7\n: @@snip [nginx-http.conf](../snippets/nginx-http.conf) \n\nNGINX L4\n: @@snip [nginx-tcp.conf](../snippets/nginx-tcp.conf) \n\nHA Proxy L7\n: @@snip [haproxy-http.conf](../snippets/haproxy-http.conf) \n\nHA Proxy L4\n: @@snip [haproxy-tcp.conf](../snippets/haproxy-tcp.conf) \n\n## Using a custom TCP load balancer\n\nYou can also use any other TCP load balancer, from a hardware box to a small js file like\n\ntcp-proxy.js\n: @@snip [tcp-proxy.js](../snippets/tcp-proxy.js) \n\ntcp-proxy.rs\n: @@snip [tcp-proxy.rs](../snippets/proxy.rs) \n\n"},{"name":"dev.md","id":"/dev.md","url":"/dev.html","title":"Developing Otoroshi","content":"# Developing Otoroshi\n\nIf you want to play with Otoroshis code, here are some tips\n\n## The tools\n\nYou will need\n\n* git\n* JDK >= 11\n* SBT >= 1.7+\n* Node 18+ & yarn 1.x\n\n## Clone the repository\n\n```sh\ngit clone https://github.com/MAIF/otoroshi.git\n```\n\nor fork otoroshi and clone your own repository.\n\n## Run otoroshi in dev mode\n\nto run otoroshi in dev mode, you'll need to run two separate process to serve the javascript UI and the server part.\n\n### Javascript side\n\njust go to `/otoroshi/javascript` and install the dependencies with\n\n```sh\nyarn install\n# or\nnpm install\n```\n\nthen run the dev server with\n\n```sh\nyarn start\n# or\nnpm run start\n```\n\n### Server side\n\nsetup SBT opts with\n\n```sh\nexport SBT_OPTS=\"-Xmx2G -Xss6M\"\n```\n\nthen just go to `/otoroshi` and run the sbt console with \n\n```sh\nsbt\n```\n\nthen in the sbt console run the following command\n\n```sh\n~reStart\n# to pass jvm args, you can use: ~reStart --- -Dotoroshi.storage=memory ...\n```\n\nyou can now access your otoroshi instance at `http://otoroshi.oto.tools:9999`\n\n## Test otoroshi\n\nto run otoroshi test just go to `/otoroshi` and run the main test suite with\n\n```sh\nsbt 'testOnly OtoroshiTests'\n```\n\n## Create a release\n\njust go to `/otoroshi/javascript` and then build the UI\n\n```sh\nyarn install\nyarn build\n```\n\nthen go to `/otoroshi` and build the otoroshi distribution\n\n```sh\nsbt ';clean;compile;dist;assembly'\n```\n\nthe otoroshi build is waiting for you in `/otoroshi/target/scala-2.12/otoroshi.jar` or `/otoroshi/target/universal/otoroshi-1.x.x.zip`\n\n## Build the documentation\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/doc.sh all\n```\n\nThe documentation is located at `manual/target/paradox/site/main/`\n\n## Format the sources\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/fmt.sh\n```\n"},{"name":"apikeys.md","id":"/entities/apikeys.md","url":"/entities/apikeys.html","title":"Apikeys","content":"# Apikeys\n\nAn API key is a unique identifier used to connect to, or perform, an route call. \n\n@@@ div { .centered-img }\n\n@@@\n\nYou can found a concrete example @ref:[here](../how-to-s/secure-with-apikey.md)\n\n* `ApiKey Id`: the id is a unique random key that will represent this API key\n* `ApiKey Secret`: the secret is a random key used to validate the API key\n* `ApiKey Name`: a name for the API key, used for debug purposes\n* `ApiKey description`: a useful description for this apikey\n* `Valid until`: auto disable apikey after this date\n* `Enabled`: if the API key is disabled, then any call using this API key will fail\n* `Read only`: if the API key is in read only mode, every request done with this api key will only work for GET, HEAD, OPTIONS verbs\n* `Allow pass by clientid only`: here you allow client to only pass client id in a specific header in order to grant access to the underlying api\n* `Constrained services only`: this apikey can only be used on services using apikey routing constraints\n* `Authorized on`: the groups/services linked to this api key\n\n### Metadata and tags\n\n* `Tags`: tags attached to the api key\n* `Metadata`: metadata attached to the api key\n\n### Automatic secret rotation\n\nAPI can handle automatic secret rotation by themselves. When enabled, the rotation changes the secret every `Rotation every` duration. During the `Grace period` both secret will be usable.\n \n* `Enabled`: enabled automatic apikey secret rotation\n* `Rotation every`: rotate secrets every\n* `Grace period`: period when both secrets can be used\n* `Next client secret`: display the next generated client secret\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Call examples\n\n* `Curl Command`: simple request with the api key passed by header\n* `Basic Auth. Header`: authorization Header with the api key as base64 encoded format\n* `Curl Command with Basic Auth. Header`: simple request with api key passed in the Authorization header as base64 format\n\n### Quotas\n\n* `Throttling quota`: the authorized number of calls per second\n* `Daily quota`: the authorized number of calls per day\n* `Monthly quota`: the authorized number of calls per month\n\n@@@ warning\n\nDaily and monthly quotas are based on the following rules :\n\n* daily quota is computed between 00h00:00.000 and 23h59:59.999 of the current day\n* monthly qutoas is computed between the first day of the month at 00h00:00.000 and the last day of the month at 23h59:59.999\n@@@\n\n### Quotas consumption\n\n* `Consumed daily calls`: the number of calls consumed today\n* `Remaining daily calls`: the remaining number of calls for today\n* `Consumed monthly calls`: the number of calls consumed this month\n* `Remaining monthly calls`: the remaining number of calls for this month\n\n"},{"name":"auth-modules.md","id":"/entities/auth-modules.md","url":"/entities/auth-modules.html","title":"Authentication modules","content":"# Authentication modules\n\nThe authentication modules manage the access to Otoroshi UI and can protect a route.\n\nA `private Otoroshi app` is an Otoroshi route with the Authentication plugin enabled.\n\nThe list of supported authentication are :\n\n* `OAuth 2.0/2.1` : an authorization standard that allows a user to grant limited access to their resources on one site to another site, without having to expose their credentials\n* `OAuth 1.0a` : the original standard for access delegation\n* `In memory` : create users directly in Otoroshi with rights and metadata\n* `LDAP : Lightweight Directory Access Protocol` : connect users using a set of LDAP servers\n* `SAML V2 - Security Assertion Markup Language` : an open-standard, XML-based data format that allows businesses to communicate user authentication and authorization information to partner companies and enterprise applications their employees may use.\n\nAll authentication modules have a unique `id`, a `name` and a `description`.\n\nEach module has also the following fields : \n\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n* `HttpOnly`: if enabled, the cookie cannot be accessed through client side script, prevent cross-site scripting (XSS) by not revealing the cookie to a third party\n* `Secure`: if enabled, avoid to include cookie in an HTTP Request without secure channel, typically HTTPs.\n* `Session max. age`: duration until the session expired\n* `User validators`: a list of validator that will check if, a user that successfully logged in has the right to actually, pass otoroshi based on the content of it's profile. A validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value. The JSONPath will be applied on a document that will look like\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"randomId\": \"xxxxx\",\n \"name\": \"john.doe@otoroshi.io\",\n \"email\": \"john.doe@otoroshi.io\",\n \"authConfigId\": \"xxxxxxxx\",\n \"profile\": { // the profile shape depends heavily on the identity provider\n \"sub\": \"xxxxxx\",\n \"nickname\": \"john.doe\",\n \"name\": \"john.doe@otoroshi.io\",\n \"picture\": \"https://foo.bar/avatar.png\",\n \"updated_at\": \"2022-04-20T12:57:39.723Z\",\n \"email\": \"john.doe@otoroshi.io\",\n \"email_verified\": true,\n \"rights\": [\"one\", \"two\"]\n },\n \"token\": { // the token shape depends heavily on the identity provider\n \"access_token\": \"xxxxxx\",\n \"refresh_token\": \"yyyyyy\",\n \"id_token\": \"zzzzzz\",\n \"scope\": \"openid profile email address phone offline_access\",\n \"expires_in\": 86400,\n \"token_type\": \"Bearer\"\n },\n \"realm\": \"global-oauth-xxxxxxx\",\n \"otoroshiData\": {\n ...\n },\n \"createdAt\": 1650459462650,\n \"expiredAt\": 1650545862652,\n \"lastRefresh\": 1650459462650,\n \"metadata\": {},\n \"tags\": []\n}\n```\n\nthe expected value support some syntax tricks like \n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current user has the right `two`, you can write the following validator\n\n```js\n{\n \"path\": \"$.profile.rights\",\n \"value\": \"Contains(two)\"\n}\n```\n\n## OAuth 2.0 / OIDC provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check these tutorials : @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md) or @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n\n* `Use cookie`: If your OAuth2 provider does not support query param in redirect uri, you can use cookies instead\n* `Use json payloads`: the access token, sended to retrieve the user info, will be pass in body as JSON. If disabled, it will sended as Map.\n* `Enabled PKCE flow`: This way, a malicious attacker can only intercept the Authorization Code, and they cannot exchange it for a token without the Code Verifier.\n* `Disable wildcard on redirect URIs`: As of OAuth 2.1, query parameters on redirect URIs are no longer allowed\n* `Refresh tokens`: Automatically refresh access token using the refresh token if available\n* `Read profile from token`: if enabled, the user profile will be read from the access token, otherwise the user profile will be retrieved from the user information url\n* `Super admins only`: All logged in users will have super admins rights\n* `Client ID`: a public identifier of your app\n* `Client Secret`: a secret known only to the application and the authorization server\n* `Authorize URL`: used to interact with the resource owner and get the authorization to access the protected resource\n* `Token URL`: used by the application in order to get an access token or a refresh token\n* `Introspection URL`: used to validate access tokens\n* `Userinfo URL`: used to retrieve the profile of the user\n* `Login URL`: used to redirect user to the login provider page\n* `Logout URL`: redirect uri used by the identity provider to redirect user after logging out\n* `Callback URL`: redirect uri sended to the identity provider to redirect user after successfully connecting\n* `Access token field name`: field used to search access token in the response body of the token URL call\n* `Scope`: presented scopes to the user in the consent screen. Scopes are space-separated lists of identifiers used to specify what access privileges are being requested\n* `Claims`: asked name/values pairs that contains information about a user.\n* `Name field name`: Retrieve name from token field\n* `Email field name`: Retrieve email from token field\n* `Otoroshi metadata field name`: Retrieve metadata from token field\n* `Otoroshi rights field name`: Retrieve user rights from user profile\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n* `Api key metadata field name`: used to extract api key metadata from the OIDC access token \n* `Api key tags field name`: used to extract api key tags from the OIDC access token \n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n* `OIDC config url`: URI of the openid-configuration used to discovery documents. By convention, this URI ends with `.well-known/openid-configuration`\n* `Token verification`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: The Hmac secret\n* `Base64 encoded secret`: Is the secret encoded with base64\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n## OAuth 1.0a provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)\n\n* `Http Method`: method used to get request token and the access token \n* `Consumer key`: the identifier portion of the client credentials (equivalent to a username)\n* `Consumer secret`: the identifier portion of the client credentials (equivalent to a password)\n* `Request Token URL`: url to retrieve the request token\n* `Authorize URL`: used to redirect user to the login page\n* `Access token URL`: used to retrieve the access token from the server\n* `Profile URL`: used to get the user profile\n* `Callback URL`: used to redirect user when successfully connecting\n* `Rights override`: override the rights of the connected user. With JSON format, each authenticated user, using email, can be associated to a list of rights on tenants and Otoroshi teams.\n\n## LDAP Authentication provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Allow empty password`: LDAP servers configured by default with the possibility to connect without password can be secured by this module to ensure that user provides a password\n* `Super admins only`: All logged in users will have super admins rights\n* `Extract profile`: extract LDAP profile in the Otoroshi user\n* `LDAP Server URL`: list of LDAP servers to join. Otoroshi use this list in sequence and swap to the next server, each time a server breaks in timeout\n* `Search Base`: used to global filter\n* `Users search base`: concat with search base to search users in LDAP\n* `Mapping group filter`: map LDAP groups with Otoroshi rights\n* `Search Filter`: used to filter users. *\\${username}* is replace by the email of the user and compare to the given field\n* `Admin username (bind DN)`: holds the name of the environment property for specifying the identity of the principal for authenticating the caller to the service\n* `Admin password`: holds the name of the environment property for specifying the credentials of the principal for authenticating the caller to the service\n* `Extract profile filters attributes in`: keep only attributes which are matching the regex\n* `Extract profile filters attributes not in`: keep only attributes which are not matching the regex\n* `Name field name`: Retrieve name from LDAP field\n* `Email field name`: Retrieve email from LDAP field\n* `Otoroshi metadata field name`: Retrieve metadata from LDAP field\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Additional rights group`: list of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n\n## In memory provider\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Login with WebAuthn` : enabled logging by WebAuthn\n* `Users`: list of users with *name*, *email* and *metadata*. The default password is *password*. The edit button is useful when you want to change the password of the user. The reset button reinitialize the password. \n* `Users raw`: show the registered users with their profile and their rights. You can edit directly each field, especially the rights of the user.\n\n## SAML v2 provider\n\n* `Single sign on URL`: the Identity Provider Single Sign-On URL\n* `The protocol binding for the login request`: the protocol binding for the login request\n* `Single Logout URL`: a SAML flow that allows the end-user to logout from a single session and be automatically logged out of all related sessions that were established during SSO\n* `The protocol binding for the logout request`: the protocol binding for the logout request\n* `Sign documents`: Should SAML Request be signed by Otoroshi ?\n* `Validate Assertions Signature`: Enable/disable signature validation of SAML assertions\n* `Validate assertions with Otoroshi certificate`: validate assertions with Otoroshi certificate. If disabled, the `Encryption Certificate` and `Encryption Private Key` fields can be used to pass a certificate and a private key to validate assertions.\n* `Encryption Certificate`: certificate used to verify assertions\n* `Encryption Private Key`: privaye key used to verify assertions\n* `Signing Certificate`: certicate used to sign documents\n* `Signing Private Key`: private key to sign documents\n* `Signature al`: the signature algorithm to use to sign documents\n* `Canonicalization Method`: canonicalization method for XML signatures \n* `Encryption KeyPair`: the keypair used to sign/verify assertions\n* `Name ID Format`: SP and IdP usually communicate each other about a subject. That subject should be identified through a NAME-IDentifier, which should be in some format so that It is easy for the other party to identify it based on the Format\n* `Use NameID format as email`: use NameID format as email. If disabled, the email will be search from the attributes\n* `URL issuer`: provide the URL to the IdP's who will issue the security token\n* `Validate Signature`: enable/disable signature validation of SAML responses\n* `Validate Assertions Signature`: should SAML Assertions to be decrypted ?\n* `Validating Certificates`: the certificate in PEM format that must be used to check for signatures.\n\n## Special routes\n\nwhen using private apps with auth. modules, you can access special routes that can help you \n\n```sh \nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/logout' # trigger logout for the current auth. module\nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/me' # get the current logged user profile (do not forget to pass cookies)\n```\n\n## Related pages\n* @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n* @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md)\n* @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n* @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)"},{"name":"backends.md","id":"/entities/backends.md","url":"/entities/backends.html","title":"Backends","content":"# Backends\n\nA backend represent a list of server to target in a route and its client settings, load balancing, etc.\n\nThe backends can be define directly on the route designer or on their dedicated page in order to be reusable.\n\n## UI page\n\nYou can find all backends [here](http://otoroshi.oto.tools:8080/bo/dashboard/backends)\n\n## Global Properties\n\n* `Targets root path`: the path to add to each request sent to the downstream service \n* `Full path rewrite`: When enabled, the path of the uri will be totally stripped and replaced by the value of `Targets root path`. If this value contains expression language expressions, they will be interpolated before forwading the request to the backend. When combined with things like named path parameters, it is possible to perform a ful url rewrite on the target path like\n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Targets\n\nThe list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures.\n\n* `id`: unique id of the target\n* `Hostname`: the hostname of the target without scheme\n* `Port`: the port of the target\n* `TLS`: call the target via https\n* `Weight`: the weight of the target. This valus is used by the load balancing strategy to dispatch the traffic between all targets\n* `Predicate`: a function to filter targets from the target list based on a predefined predicate\n* `Protocol`: protocol used to call the target, can be only equals to `HTTP/1.0`, `HTTP/1.1`, `HTTP/2.0` or `HTTP/3.0`\n* `IP address`: the ip address of the target\n* `TLS Settings`:\n * `Enabled`: enable this section\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with the downstream service\n * `Trusted certificates`: list of trusted certificates received from the downstream service\n\n\n## Heatlh check\n\n* `Enabled`: if enabled, the health check URL will be called at regular intervals\n* `URL`: the URL to call to run the health check\n\n## Load balancing\n\n* `Type`: the load balancing algorithm used\n\n## Client settings\n\n* `backoff factor`: specify the factor to multiply the delay for each retry (default value 2)\n* `retries`: specify how many times the client will retry to fetch the result of the request after an error before giving up. (default value 1)\n* `max errors`: specify how many errors can pass before opening the circuit breaker (default value 20)\n* `global timeout`: specify how long the global call (with retries) should last at most in milliseconds. (default value 30000)\n* `connection timeout`: specify how long each connection should last at most in milliseconds. (default value 10000)\n* `idle timeout`: specify how long each connection can stay in idle state at most in milliseconds (default value 60000)\n* `call timeout`: Specify how long each call should last at most in milliseconds. (default value 30000)\n* `call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response. (default value 120000)\n* `initial delay`: delay after which first retry will happens if needed (default value 50)\n* `sample interval`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor (default value 2000)\n* `cache connection`: try to keep tcp connection alive between requests (default value false)\n* `cache connection queue size`: queue size for an open tcp connection (default value 2048)\n* `custom timeouts` (list): \n * `Path`: the path on which the timeout will be active\n * `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n * `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n * `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n * `Call timeout`: Specify how long each call should last at most in milliseconds.\n * `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n## Proxy\n\n* `host`: host of proxy behind the identify provider\n* `port`: port of proxy behind the identify provider\n* `protocol`: protocol of proxy behind the identify provider\n* `principal`: user of proxy \n* `password`: password of proxy\n"},{"name":"certificates.md","id":"/entities/certificates.md","url":"/entities/certificates.html","title":"Certificates","content":"# Certificates\n\nAll generated and imported certificates are listed in the `https://otoroshi.xxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe list of available actions are:\n\n* `Add item`: redirects the user on the certificate creation page. It's useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* `Let's Encrypt certificate`: asks a certificate matching a given host to Let's encrypt \n* `Create certificate`: issues a certificate with an existing Otoroshi certificate as CA.\n* `Import .p12 file`: loads a p12 file as certificate\n\n## Add item\n\n* `Id`: the generated unique id of the certificate\n* `Name`: the name of the certificate\n* `Description`: the description of the certificate\n* `Auto renew cert.`: certificate will be issued when it will be expired. Only works with a CA from Otoroshi and a known private key\n* `Client cert.`: the certificate generated will be used to identicate a client to a server\n* `Keypair`: the certificate entity will be a pair of public key and private key.\n* `Public key exposed`: if true, the public key will be exposed on `http://otoroshi-api.your-domain/.well-known/jwks.json`\n* `Certificate status`: the current status of the certificate. It can be valid if the certificate is not revoked and not expired, or equal to the reason of the revocation\n* `Certificate full chain`: list of certificates used to authenticate a client or a server\n* `Certificate private key`: the private key of the certificate or nothing if wanted. You can omit it if you want just add a certificte full chain to trust them.\n* `Private key password`: the password to protect the private key\n* `Certificate tags`: the tags attached to the certificate\n* `Certaificate metadata`: the metadata attached to the certificate\n\n## Let's Encrypt certificate\n\n* `Let's encrypt`: if enabled, the certificate will be generated by Let's Encrypt. If disabled, the user will be redirect to the `Create certificate` page\n* `Host`: the host send to Let's encrypt to issue the certificate\n\n## Create certificate view\n\n* `Issuer`: the CA used to sign your certificate\n* `CA certificate`: if enabled, the certificate will be used as an authority certificate. Once generated, it will be use as CA to sign the new certificates\n* `Let's Encrypt`: redirects to the Let's Encrypt page to request a certificate\n* `Client certificate`: the certificate generated will be used to identicate a client to a server\n* `Include A.I.A`: include authority information access urls in the certificate\n* `Key Type`: the type of the private key\n* `Key Size`: the size of the private key\n* `Signature Algorithm`: the signature algorithm used to sign the certificate\n* `Digest Algorithm`: the digest algorithm used\n* `Validity`: how much time your certificate will be valid\n* `Subject DN`: the subject DN of your certificate\n* `Hosts`: the hosts of your certificate\n\n"},{"name":"data-exporters.md","id":"/entities/data-exporters.md","url":"/entities/data-exporters.html","title":"Data exporters","content":"# Data exporters\n\nThe data exporters are the way to export alerts and events from Otoroshi to an external storage.\n\nTo try them, you can folllow @ref[this tutorial](../how-to-s/export-alerts-using-mailgun.md).\n\n## Common fields\n\n* `Type`: the type of event exporter\n* `Enabled`: enabled or not the exporter\n* `Name`: given name to the exporter\n* `Description`: the data exporter description\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nAll exporters are split in three parts. The first and second parts are common and the last are specific by exporter.\n\n* `Filtering and projection` : section to filter the list of sent events and alerts. The projection field allows you to export only certain event fields and reduce the size of exported data. It's composed of `Filtering` and `Projection` fields. To get a full usage of this elements, read @ref:[this section](#matching-and-projections)\n* `Queue details`: set of fields to adjust the workers of the exporter. \n * `Buffer size`: if elements are pushed onto the queue faster than the source is consumed the overflow will be handled with a strategy specified by the user. Keep in memory the number of events.\n * `JSON conversion workers`: number of workers used to transform events to JSON format in paralell\n * `Send workers`: number of workers used to send transformed events\n * `Group size`: chunk up this stream into groups of elements received within a time window (the time window is the next field)\n * `Group duration`: waiting time before sending the group of events. If the group size is reached before the group duration, the events will be instantly sent\n \nFor the last part, the `Exporter configuration` will be detail individually.\n\n## Matching and projections\n\n**Filtering** is used to **include** or **exclude** some kind of events and alerts. For each include and exclude field, you can add a list of key-value. \n\nLet's say we only want to keep Otoroshi alerts\n```json\n{ \"include\": [{ \"@type\": \"AlertEvent\" }] }\n```\n\nOtoroshi provides a list of rules to keep only events with specific values. We will use the following event to illustrate.\n\n```json\n{\n \"foo\": \"bar\",\n \"type\": \"AlertEvent\",\n \"alert\": \"big-alert\",\n \"status\": 200,\n \"codes\": [\"a\", \"b\"],\n \"inner\": {\n \"foo\": \"bar\",\n \"bar\": \"foo\"\n }\n}\n```\n\nThe rules apply with the previous example as event.\n\n@@@div { #filtering }\n \n@@@\n\n\n\n**Projection** is a list of fields to export. In the case of an empty list, all the fields of an event will be exported. In other case, **only** the listed fields will be exported.\n\nLet's say we only want to keep Otoroshi alerts and only type, timestamp and id of each exported events\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nAn other possibility is to **rename** the exported field. This value will be the same but the exported field will have a different name.\n\nLet's say we want to rename all `@id` field with `unique-id` as key\n\n```json\n{ \"@id\": \"unique-id\" }\n```\n\nThe last possiblity is to retrieve a sub-object of an event. Let's say we want to get the name of each exported user of events.\n\n```json\n{ \"user\": { \"name\": true } }\n```\n\nYou can also expand the entire source object with \n\n```json\n{\n \"$spread\": true\n}\n```\n\nand the remove fields you don't want with \n\n```json\n{\n \"fieldthatidontwant\": false\n}\n```\n\nProjections allows object modification using jspath, for instance, this example will create a new `otoroshiHeaderKeys` field to exported events. This field will contains a string array containing every request header name.\n\n```json\n{\n \"otoroshiHeaderKeys\": {\n \"$path\": \"$.otoroshiHeadersIn.*.key\"\n }\n}\n```\n\nAlternativerly, projections also allow to use JQ to transform exported events\n\n```json\n{\n \"headerKeys\": {\n \"$jq\": \"[.headers[].key]\"\n }\n}\n```\n\nJQ filter also allows conditionnal filtering : transformation is applied only if given predicate is match. In the following example, `headerKeys` field will be valued only if `target.scheme` is `https`.\n\n```json\n{\n \"headerKeys\": {\n \"$jqIf\": {\n \"filter\": \"[.headers[].key]\",\n \"predicate\": {\n \"path\": \"target.scheme\",\n \"value\": \"https\"\n }\n }\n }\n}\n```\n\nSee [JQ manual](https://jqlang.github.io/jq/manual/) for complete syntax reference.\n\n## Elastic\n\nWith this kind of exporter, every matching event will be sent to an elastic cluster (in batch). It is quite useful and can be used in combination with [elastic read in global config](./global-config.html#analytics-elastic-dashboard-datasource-read-)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with checked point, and if the case of it's successfull, it will displayed the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template presents in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n## Webhook \n\nWith this kind of exporter, every matching event will be sent to a URL (in batch) using a POST method and an JSON array body.\n\n* `Alerts hook URL`: url used to post events\n* `Hook Headers`: headers add to the post request\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n## Pulsar \n\nWith this kind of exporter, every matching event will be sent to an [Apache Pulsar topic](https://pulsar.apache.org/)\n\n\n* `Pulsar URI`: URI of the pulsar server\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n* `Pulsar tenant`: tenant on the pulsar server\n* `Pulsar namespace`: namespace on the pulsar server\n* `Pulsar topic`: topic on the pulsar server\n\n## Kafka \n\nWith this kind of exporter, every matching event will be sent to an [Apache Kafka topic](https://kafka.apache.org/). You can find few @ref[tutorials](../how-to-s/communicate-with-kafka.md) about the connection between Otoroshi and Kafka based on docker images.\n\n* `Kafka Servers`: the list of servers to contact to connect the Kafka client with the Kafka cluster\n* `Kafka topic`: the topic on which Otoroshi alerts will be sent\n\nBy default, Kafka is installed with no authentication. Otoroshi supports the following authentication mechanisms and protocols for Kafka brokers.\n\n### SASL\n\nThe Simple Authentication and Security Layer (SASL) [RFC4422] is a\nmethod for adding authentication support to connection-based\nprotocols.\n\n* `SASL username`: the client username \n* `SASL password`: the client username \n* `SASL Mechanism`: \n * `PLAIN`: SASL/PLAIN uses a simple username and password for authentication.\n * `SCRAM-SHA-256` and `SCRAM-SHA-512`: SASL/SCRAM uses usernames and passwords stored in ZooKeeper. Credentials are created during installation.\n\n### SSL \n\n* `Kafka keypass`: the keystore password if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka keystore path`: the keystore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka truststore path`: the truststore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Custom TLS Settings`: enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n### SASL + SSL\n\nThis mechanism uses the SSL configuration and the SASL configuration.\n\n## Mailer \n\nWith this kind of exporter, every matching event will be sent in batch as an email (using one of the following email provider)\n\nOtoroshi supports 5 exporters of email type.\n\n### Console\n\nNothing to add. The events will be write on the standard output.\n\n### Generic\n\n* `Mailer url`: URL used to push events\n* `Headers`: headers add to the push requests\n* `Email addresses`: recipients of the emails\n\n### Mailgun\n\n* `EU`: is EU server ? if enabled, *https://api.eu.mailgun.net/* will be used, otherwise, the US URL will be used : *https://api.mailgun.net/*\n* `Mailgun api key`: API key of the mailgun account\n* `Mailgun domain`: domain name of the mailgun account\n* `Email addresses`: recipients of the emails\n\n### Mailjet\n\n* `Public api key`: public key of the mailjet account\n* `Private api key`: private key of the mailjet account\n* `Email addresses`: recipients of the emails\n\n### Sendgrid\n\n* `Sendgrid api key`: api key of the sendgrid account\n* `Email addresses`: recipients of the emails\n\n## File \n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n\n## GoReplay file\n\nWith this kind of exporter, every matching event will be sent to a `.gor` file compatible with [GoReplay](https://goreplay.org/). \n\n@@@ warning\nthis exporter will only be able to catch `TrafficCaptureEvent`. Those events are created when a route (or the global config) of the @ref:[new proxy engine](../topics/engine.md) is setup to capture traffic using the `capture` flag.\n@@@\n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n* `Capture requests`: capture http requests in the `.gor` file\n* `Capture responses`: capture http responses in the `.gor` file\n\n## Console \n\nNothing to add. The events will be write on the standard output.\n\n## Custom \n\nThis type of exporter let you the possibility to write your own exporter with your own rules. To create an exporter, we need to navigate to the plugins page, and to create a new item of type exporter.\n\nWhen it's done, the exporter will be visible in this list.\n\n* `Exporter config.`: the configuration of the custom exporter.\n\n## Metrics \n\nThis plugin is useful to rewrite the metric labels exposed on the `/metrics` endpoint.\n\n* `Labels`: list of metric labels. Each pair contains an existing field name and the new name."},{"name":"global-config.md","id":"/entities/global-config.md","url":"/entities/global-config.html","title":"Global config","content":"# Global config\n\nThe global config, named `Danger zone` in Otoroshi, is the place to configure Otoroshi globally. \n\n> Warning: In this page, the configuration is really sensitive and affects the global behaviour of Otoroshi.\n\n\n### Misc. Settings\n\n\n* `Maintenance mode` : It passes every single service in maintenance mode. If a user calls a service, the maintenance page will be displayed\n* `No OAuth login for BackOffice` : Forces admins to login only with user/password or user/password/u2F device\n* `API Read Only`: Freeze Otoroshi datastore in read only mode. Only people with access to the actual underlying datastore will be able to disable this.\n* `Auto link default` : When no group is specified on a service, it will be assigned to default one\n* `Use circuit breakers` : Use circuit breaker on all services\n* `Use new http client as the default Http client` : All http calls will use the new http client by default\n* `Enable live metrics` : Enable live metrics in the Otoroshi cluster. Performs a lot of writes in the datastore\n* `Digitus medius` : Use middle finger emoji as a response character for endless HTTP responses (see [IP address filtering settings](#ip-address-filtering-settings)).\n* `Limit conc. req.` : Limit the number of concurrent request processed by Otoroshi to a certain amount. Highly recommended for resilience\n* `Use X-Forwarded-* headers for routing` : When evaluating routing of a request, X-Forwarded-* headers will be used if presents\n* `Max conc. req.` : Maximum number of concurrent requests processed by otoroshi.\n* `Max HTTP/1.0 resp. size` : Maximum size of an HTTP/1.0 response in bytes. After this limit, response will be cut and sent as is. The best value here should satisfy (maxConcurrentRequests * maxHttp10ResponseSize) < process.memory for worst case scenario.\n* `Max local events` : Maximum number of events stored.\n* `Lines` : *deprecated* \n\n### IP address filtering settings\n\n* `IP allowed list`: Only IP addresses that will be able to access Otoroshi exposed services\n* `IP blocklist`: IP addresses that will be refused to access Otoroshi exposed services\n* `Endless HTTP Responses`: IP addresses for which each request will return around 128 Gb of 0s\n\n\n### Quotas settings\n\n* `Global throttling`: The max. number of requests allowed per second globally on Otoroshi\n* `Throttling per IP`: The max. number of requests allowed per second per IP address globally on Otoroshi\n\n### Analytics: Elastic dashboard datasource (read)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with a connection checklist, if connection is successfull, it will display the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template present in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes over time. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `TrustAll`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with elasticsearch\n* `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n### Statsd settings\n\n* `Datadog agent`: The StatsD agent is a Datadog agent\n* `StatsD agent host`: The host on which StatsD agent is listening\n* `StatsD agent port`: The port on which StatsD agent is listening (default is 8125)\n\n\n### Backoffice auth. settings\n\n* `Backoffice auth. config`: the authentication module used in front of Otoroshi. It will be used to connect to Otoroshi on the login page\n\n### Let's encrypt settings\n\n* `Enabled`: when enabled, Otoroshi will have the possiblity to sign certificate from let's encrypt notably in the SSL/TSL Certificates page \n* `Server URL`: ACME endpoint of let's encrypt \n* `Email addresses`: (optional) list of addresses used to order the certificates \n* `Contact URLs`: (optional) list of addresses used to order the certificates \n* `Public Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n* `Private Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n\n\n### CleverCloud settings\n\nOnce configured, you can register one clever cloud app of your organization directly as an Otoroshi service.\n\n* `CleverCloud consumer key`: consumer key of your clever cloud OAuth 1.0 app\n* `CleverCloud consumer secret`: consumer secret of your clever cloud OAuth 1.0 app\n* `OAuth Token`: oauth token of your clever cloud OAuth 1.0 app\n* `OAuth Secret`: oauth token secret of your clever cloud OAuth 1.0 app \n* `CleverCloud orga. Id`: id of your clever cloud organization\n\n### Global scripts\n\nGlobal scripts will be deprecated soon, please use global plugins instead (see the next section)!\n\n### Global plugins\n\n* `Enabled`: enable the use of global plugins\n* `Plugins on new Otoroshi engine`: list of plugins used by the new Otoroshi engine\n* `Plugins on old Otoroshi engine`: list of plugins used by the old Otoroshi engine\n* `Plugin configuration`: the overloaded configuration of plugins\n\n### Proxies\n\nIn this section, you can add a list of proxies for :\n\n* Proxy for alert emails (mailgun)\n* Proxy for alert webhooks\n* Proxy for Clever-Cloud API access\n* Proxy for services access\n* Proxy for auth. access (OAuth, OIDC)\n* Proxy for client validators\n* Proxy for JWKS access\n* Proxy for elastic access\n\nEach proxy has the following fields \n\n* `Proxy host`: host of proxy\n* `Proxy port`: port of proxy\n* `Proxy principal`: user of proxy\n* `Proxy password`: password of proxy\n* `Non proxy host`: IP address that can access the service\n\n### Quotas alerting settings\n\n* `Enable quotas exceeding alerts`: When apikey quotas is almost exceeded, an alert will be sent \n* `Daily quotas threshold`: The percentage of daily calls before sending alerts\n* `Monthly quotas threshold`: The percentage of monthly calls before sending alerts\n\n### User-Agent extraction settings\n\n* `User-Agent extraction`: Allow user-agent details extraction. Can have impact on consumed memory. \n\n### Geolocation extraction settings\n\nExtract a geolocation for each call to Otoroshi.\n\n### Tls Settings\n\n* `Use random cert.`: Use the first available cert when none matches the current domain\n* `Default domain`: When the SNI domain cannot be found, this one will be used to find the matching certificate \n* `Trust JDK CAs (server)`: Trust JDK CAs. The CAs from the JDK CA bundle will be proposed in the certificate request when performing TLS handshake \n* `Trust JDK CAs (trust)`: Trust JDK CAs. The CAs from the JDK CA bundle will be used as trusted CAs when calling HTTPS resources \n* `Trusted CAs (server)`: Select the trusted CAs you want for TLS terminaison. Those CAs only will be proposed in the certificate request when performing TLS handshake \n\n\n### Auto Generate Certificates\n\n* `Enabled`: Generate certificates on the fly when they don't exist\n* `Reply Nicely`: When receiving request from a not allowed domain name, accept connection and display a nice error message \n* `CA`: certificate CA used to generate missing certificate\n* `Allowed domains`: Allowed domains\n* `Not allowed domains`: Not allowed domains\n \n\n### Global metadata\n\n* `Tags`: tags attached to the global config\n* `Metadata`: metadata attached to the global config\n\n### Actions at the bottom of the page\n\n* `Recover from a full export file`: Load global configuration from a previous export\n* `Full export`: Export with all created entities\n* `Full export (ndjson)`: Export your full state of database to ndjson format\n* `JSON`: Get the global config at JSON format \n* `YAML`: Get the global config at YAML format \n* `Enable Panic Mode`: Log out all users from UI and prevent any changes to the database by setting the admin Otoroshi api to read-only. The only way to exit of this mode is to disable this mode directly in the database. "},{"name":"index.md","id":"/entities/index.md","url":"/entities/index.html","title":"","content":"\n# Main entities\n\nIn this section, we will pass through all the main Otoroshi entities. Otoroshi entities are the main items stored in otoroshi datastore that will be used to configure routing, authentication, etc.\n\nAny entity has the following properties\n\n* **location** or **\\_loc**: the location of the entity (organization and team)\n* **id**: the id of the entity (except for apikeys)\n* **name**: the name of the entity\n* **description**: the description of the entity (optional)\n* **tags**: free tags that you can put on any entity to help you manage it, automate it, etc.\n* **metadata**: free key/value tuples that you can put on any entity to help you manage it, automate it, etc.\n\n@@@div { .entities }\n\n
\nRoutes\nProxy your applications with routes\n
\n@ref:[View](./routes.md)\n@@@\n\n@@@div { .entities }\n\n
\nBackends\nReuse route targets\n
\n@ref:[View](./backends.md)\n@@@\n\n@@@div { .entities }\n\n
\nApikeys\nAdd security to your services using apikeys\n
\n@ref:[View](./apikeys.md)\n@@@\n\n\n@@@div { .entities }\n\n
\nOrganizations\nThis the most high level for grouping resources.\n
\n@ref:[View](./organizations.md)\n@@@\n\n@@@div { .entities }\n\n
\nTeams\nOrganize your resources by teams\n
\n@ref:[View](./teams.md)\n@@@\n\n@@@div { .entities }\n\n
\nService groups\nGroup your services\n
\n@ref:[View](./service-groups.md)\n@@@\n\n@@@div { .entities }\n\n
\nJWT verifiers\nVerify and forge token by services.\n
\n@ref:[View](./jwt-verifiers.md)\n@@@\n\n@@@div { .entities }\n\n
\nGlobal Config\nThe danger zone of Otoroshi\n
\n@ref:[View](./global-config.md)\n@@@\n\n@@@div { .entities }\n\n
\nTCP services\n\n
\n@ref:[View](./tcp-services.md)\n@@@\n\n@@@div { .entities }\n\n
\nAuth. modules\nSecure the Otoroshi UI and your web apps\n
\n@ref:[View](./auth-modules.md)\n@@@\n\n@@@div { .entities }\n\n
\nCertificates\nAdd secure communication between Otoroshi, clients and services\n
\n@ref:[View](./certificates.md)\n@@@\n\n@@@div { .entities }\n\n
\nData exporters\nExport alerts, events ands logs\n
\n@ref:[View](./data-exporters.md)\n@@@\n\n@@@div { .entities }\n\n
\nScripts\n\n
\n@ref:[View](./scripts.md)\n@@@\n\n@@@div { .entities }\n\n
\nService descriptors\nProxy your applications with service descriptors\n
\n@ref:[View](./service-descriptors.md)\n@@@\n\n@@@ index\n\n* [Routes](./routes.md)\n* [Backends](./backends.md)\n* [Organizations](./organizations.md)\n* [Teams](./teams.md)\n* [Global Config](./global-config.md)\n* [Apikeys](./apikeys.md)\n* [Service groups](./service-groups.md)\n* [Auth. modules](./auth-modules.md)\n* [Certificates](./certificates.md)\n* [JWT verifiers](./jwt-verifiers.md)\n* [Data exporters](./data-exporters.md)\n* [Scripts](./scripts.md)\n* [TCP services](./tcp-services.md)\n* [Service descriptors](./service-descriptors.md)\n\n@@@\n"},{"name":"jwt-verifiers.md","id":"/entities/jwt-verifiers.md","url":"/entities/jwt-verifiers.html","title":"JWT verifiers","content":"# JWT verifiers\n\nSometimes, it can be pretty useful to verify Jwt tokens coming from other provider on some services. Otoroshi provides a tool to do that per service.\n\n* `Name`: name of the JWT verifier\n* `Description`: a simple description\n* `Strict`: if not strict, request without JWT token will be allowed to pass. This option is helpful when you want to force the presence of tokens in each request on a specific service \n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nEach JWT verifier is configurable in three steps : the `location` where find the token in incoming requests, the `validation` step to check the signature and the presence of claims in tokens, and the last step, named `Strategy`.\n\n## Token location\n\nAn incoming token can be found in three places.\n\n#### In query string\n\n* `Source`: JWT token location in query string\n* `Query param name`: the name of the query param where JWT is located\n\n#### In a header\n\n* `Source`: JWT token location in a header\n* `Header name`: the name of the header where JWT is located\n* `Remove value`: when the token is read, this value will be remove of header value (example: if the header value is *Bearer xxxx*, the *remove value* could be Bearer  don't forget the space at the end of the string)\n\n#### In a cookie\n\n* `Source`: JWT token location in a cookie\n* `Cookie name`: the name of the cookie where JWT is located\n\n## Token validation\n\nThis section is used to verify the extracted token from specified location.\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n\nAccording to the selected algorithm, the validation form will change.\n\n#### Hmac + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n#### RSASSA-PKCS1 + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the RSA public key\n* `Private key`: the RSA private key that can be empty if not used for JWT token signing\n\n#### ECDSA + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the ECDSA public key\n* `Private key`: the ECDSA private key that can be empty if not used for JWT token signing\n\n#### RSASSA-PKCS1 + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n \n#### ECDSA + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n\n#### Otoroshi KeyPair from token kid (only for verification)\n* `Use only exposed keypairs`: if enabled, Otoroshi will only use the key pairs that are exposed on the well-known. If disabled, it will search on any registered key pairs.\n\n#### JWK Set (only for verification)\n\n* `URL`: the JWK set URL where the public keys are exposed\n* `HTTP call timeout`: timeout for fetching the keyset\n* `TTL`: cache TTL for the keyset\n* `HTTP Headers`: the HTTP headers passed\n* `Key type`: type of the key searched in the jwks\n\n*TLS settings for JWKS fetching*\n\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n*Proxy*\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n## Strategy\n\nThe first step is to select the verifier strategy. Otoroshi supports 4 types of JWT verifiers:\n\n* `Default JWT token` will add a token if no present. \n* `Verify JWT token` will only verifiy token signing and fields values if provided. \n* `Verify and re-sign JWT token` will verify the token and will re-sign the JWT token with the provided algo. settings. \n* `Verify, re-sign and transform JWT token` will verify the token, re-sign and will be able to transform the token.\n\nAll verifiers has the following properties: \n\n* `Verify token fields`: when the JWT token is checked, each field specified here will be verified with the provided value\n* `Verify token array value`: when the JWT token is checked, each field specified here will be verified if the provided value is contained in the array\n\n\n#### Default JWT token\n\n* `Strict`: if token is already present, the call will fail\n* `Default value`: list of claims of the generated token. These fields support raw values or language expressions. See the documentation about @ref:[the expression language](../topics/expression-language.md)\n\n#### Verify JWT token\n\nNo specific values needed. This kind of verifier needs only the two fields `Verify token fields` and `Verify token array value`.\n\n#### Verify and re-sign JWT token\n\nWhen `Verify and re-sign JWT token` is chosen, the `Re-sign settings` appear. All fields of `Re-sign settings` are the same of the `Token validation` section. The only difference is that the values are used to sign the new token and not to validate the token.\n\n\n#### Verify, re-sign and transform JWT token\n\nWhen `Verify, re-sign and transform JWT token` is chosen, the `Re-sign settings` and `Transformation settings` appear.\n\nThe `Re-sign settings` are used to sign the new token and has the same fields than the `Token validation` section.\n\nFor the `Transformation settings` section, the fields are:\n\n* `Token location`: the location where to find/set the JWT token\n* `Header name`: the name of the header where JWT is located\n* `Prepend value`: remove a value inside the header value\n* `Rename token fields`: when the JWT token is transformed, it is possible to change a field name, just specify origin field name and target field name\n* `Set token fields`: when the JWT token is transformed, it is possible to add new field with static values, just specify field name and value\n* `Remove token fields`: when the JWT token is transformed, it is possible to remove fields"},{"name":"organizations.md","id":"/entities/organizations.md","url":"/entities/organizations.html","title":"Organizations","content":"# Organizations\n\nThe resources of Otoroshi are grouped by `Organization`. This the highest level for grouping resources.\n\nAn organization have a unique `id`, a `name` and a `description`. As all Otoroshi resources, an Organization have a list of tags and metadata associated.\n\nFor example, you can use the organizations as a mean of :\n\n* to seperate resources by services or entities in your enterprise\n* to split internal and external usage of the resources (it's useful when you have a list of services deployed in your company and another one deployed by your partners)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of organizations\n\nTo visualize and edit the list of organizations, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/organizations` route or click on the cog icon and select the organizations button.\n\nOnce on the page, you can create a new item, edit an existing organization or delete an existing one.\n\n> When an organization is deleted, the resources associated are not deleted. On the other hand, the organization and the team of associated resources are let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of one organization (`tenant` in the json document)\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": ...\n }\n ...\n}\n```\n\nor all organizations\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"*\",\n \"teams\": ...\n }\n ...\n}\n```\n\n"},{"name":"routes.md","id":"/entities/routes.md","url":"/entities/routes.html","title":"Routes","content":"# Routes\n\nA route is an unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins and eventually forward the request to the backend application.\n\n## UI page\n\nYou can find all routes [here](http://otoroshi.oto.tools:8080/bo/dashboard/routes)\n\n## Global Properties\n\n* `location`: the location of the entity\n* `id`: the id of the route\n* `name`: the name of the route\n* `description`: the description of the route\n* `tags`: the tags of the route. can be useful for api automation\n* `metadata`: the metadata of the route. can be useful for api automation. There are a few reserved metadata used by otorshi that can be found @ref[below](./routes.md#reserved-metadata)\n* `enabled`: is the route enabled ? if not, the router will not consider this route\n* `debugFlow`: the debug flag. If enabled, the execution report for this route will contain all input/output values through steps of the proxy engine. For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `capture`: if enabled, otoroshi will generate events containing the whole content of each request. Use with caution ! For more informations, check the @ref[engine documentation](../topics/engine.md#http-traffic-capture)\n* `exportReporting`: if enabled, execution reports of the proxy engine will be generated for each request. Those reports are exportable using @ref[data exporters](./data-exporters.md) . For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `groups`: each route is attached to a group. A group can have one or more services/routes. Each API key is linked to groups/routes/services and allow access to every entities in the groups.\n\n### Reserved metadata\n\nsome metadata are reserved for otoroshi usage. Here is the list of reserved metadata\n\n* `otoroshi-core-user-facing`: is this a user facing app for the snow monkey\n* `otoroshi-core-use-akka-http-client`: use the pure akka http client\n* `otoroshi-core-use-netty-http-client`: use the pure netty http client\n* `otoroshi-core-use-akka-http-ws-client`: use the modern websocket client\n* `otoroshi-core-issue-lets-encrypt-certificate`: enabled let's encrypt certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate`: enabled certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate-ca`: the id of the CA cert to generate the certificate for this route\n* `otoroshi-core-openapi-url`: the openapi url for this route\n* `otoroshi-core-env`: the env for this route. here for legacy reasons\n* `otoroshi-deployment-providers`: in the case of relay routing, the providers for this route\n* `otoroshi-deployment-regions`: in the case of relay routing, the network regions for this route\n* `otoroshi-deployment-zones`: in the case of relay routing, the network zone for this route \n* `otoroshi-deployment-dcs`: in the case of relay routing, the datacenter for this route \n* `otoroshi-deployment-racks`: in the case of relay routing, the rack for this route \n\n## Frontend configuration\n\n* `frontend`: the frontend of the route. It's the configuration that will configure how otoroshi router will match this route. A frontend has the following shape. \n\n```javascript\n{\n \"domains\": [ // the matched domains and paths\n \"new-route.oto.tools/path\" // here you can use wildcard in domain and path, also you can use named path params\n ],\n \"strip_path\": true, // is the matched path stripped in the forwarded request\n \"exact\": false, // perform exact matching on path, if not, will be matched on /path*\n \"headers\": {}, // the matched http headers. if none provided, any header will be matched\n \"query\": {}, // the matched http query params. if none provided, any query params will be matched\n \"methods\": [] // the matched http methods. if none provided, any method will be matched\n}\n```\n\nFor more informations about routing, check the @ref[engine documentation](../topics/engine.md#routing)\n\n## Backend configuration\n\n* `backend`: a backend to forward requests to. For more informations, go to the @ref[backend documentation](./backends.md)\n* `backendRef`: a reference to an existing backend id\n\n## Plugins\n\nthe liste of plugins used on this route. Each plugin definition has the following shape:\n\n```javascript\n{\n \"enabled\": false, // is the plugin enabled\n \"debug\": false, // is debug enabled of this specific plugin\n \"plugin\": \"cp:otoroshi.next.plugins.Redirection\", // the id of the plugin\n \"include\": [], // included paths. if none, all paths are included\n \"exclude\": [], // excluded paths. if none, none paths are excluded\n \"config\": { // the configuration of the plugin\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"plugin_index\": { // the position of the plugin. if none provided, otoroshi will use the order in the plugin array\n \"pre_route\": 0\n }\n}\n```\n\nfor more informations about the available plugins, go @ref[here](../plugins/built-in-plugins.md)\n\n\n"},{"name":"scripts.md","id":"/entities/scripts.md","url":"/entities/scripts.html","title":"Scripts","content":"# Scripts\n\nScript are a way to create plugins for otoroshi without deploying them as jar files. With scripts, you just have to store the scala code of your plugins inside the otoroshi datastore and otoroshi will compile and deploy them at startup. You can find all your scripts in the UI at `cog icon / Plugins`. You can find all the documentation about plugins @ref:[here](../plugins/index.md)\n\n@@@ warning\nThe compilation of your plugins can be pretty long and resources consuming. As the compilation happens during otoroshi boot sequence, your instance will be blocked until all plugins have compiled. This behavior can be disabled. If so, the plugins will not work until they have been compiled. Any service using a plugin that is not compiled yet will fail\n@@@\n\nLike any entity, the script has has the following properties\n\n* `id`\n* `plugin name`\n* `plugin description`\n* `tags`\n* `metadata`\n\nAnd you also have\n\n* `type`: the kind of plugin you are building with this script\n* `plugin code`: the code for your plugin\n\n## Compile\n\nYou can use the compile button to check if the code you write in `plugin code` is valid. It will automatically save your script and try to compile. As mentionned earlier, script compilation is quite resource intensive. It will affect your CPU load and your memory consumption. Don't forget to adjust your VM settings accordingly.\n"},{"name":"service-descriptors.md","id":"/entities/service-descriptors.md","url":"/entities/service-descriptors.html","title":"Service descriptors","content":"# Service descriptors\n\nServices or service descriptor, let you declare how to proxy a call from a domain name to another domain name (or multiple domain names). \n\n@@@ div { .centered-img }\n\n@@@\n\nLet’s say you have an API exposed on http://192.168.0.42 and I want to expose it on https://my.api.foo. Otoroshi will proxy all calls to https://my.api.foo and forward them to http://192.168.0.42. While doing that, it will also log everyhting, control accesses, etc.\n\n\n* `Id`: a unique random string to identify your service\n* `Groups`: each service descriptor is attached to a group. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group.\n* `Create a new group`: you can create a new group to host this descriptor\n* `Create dedicated group`: you can create a new group with an auto generated name to host this descriptor\n* `Name`: the name of your service. Only for debug and human readability purposes.\n* `Description`: the description of your service. Only for debug and human readability purposes.\n* `Service enabled`: activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist.\n* `Read only mode`: authorize only GET, HEAD, OPTIONS calls on this service\n* `Maintenance mode`: display a maintainance page when a user try to use the service\n* `Construction mode`: display a construction page when a user try to use the service\n* `Log analytics`: Log analytics events for this service on the servers\n* `Use new http client`: will use Akka Http Client for every request\n* `Detect apikey asap`: If the service is public and you provide an apikey, otoroshi will detect it and validate it. Of course this setting may impact performances because of useless apikey lookups.\n* `Send Otoroshi headers back`: when enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...\n* `Override Host header`: when enabled, Otoroshi will automatically set the Host header to corresponding target host\n* `Send X-Forwarded-* headers`: when enabled, Otoroshi will send X-Forwarded-* headers to target\n* `Force HTTPS`: will force redirection to `https://` if not present\n* `Allow HTTP/1.0 requests`: will return an error on HTTP/1.0 request\n* `Use new WebSocket client`: will use the new websocket client for every websocket request\n* `TCP/UDP tunneling`: with this setting enabled, otoroshi will not proxy http requests anymore but instead will create a secured tunnel between a cli on your machine and otoroshi to proxy any tcp connection with all otoroshi security features enabled\n\n### Service exposition settings\n\n* `Exposed domain`: the domain used to expose your service. Should follow pattern: `(http|https)://subdomain?.env?.domain.tld?/root?` or regex `(http|https):\\/\\/(.*?)\\.?(.*?)\\.?(.*?)\\.?(.*)\\/?(.*)`\n* `Legacy domain`: use `domain`, `subdomain`, `env` and `matchingRoot` for routing in addition to hosts, or just use hosts.\n* `Strip path`: when matching, strip the matching prefix from the upstream request URL. Defaults to true\n* `Issue Let's Encrypt cert.`: automatically issue and renew let's encrypt certificate based on domain name. Only if Let's Encrypt enabled in global config.\n* `Issue certificate`: automatically issue and renew a certificate based on domain name\n* `Possible hostnames`: all the possible hostnames for your service\n* `Possible matching paths`: all the possible matching paths for your service\n\n### Redirection\n\n* `Redirection enabled`: enabled the redirection. If enabled, a call to that service will redirect to the chosen URL\n* `Http redirection code`: type of redirection used\n* `Redirect to`: URL used to redirect user when the service is called\n\n### Service targets\n\n* `Redirect to local`: if you work locally with Otoroshi, you may want to use that feature to redirect one specific service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests\n* `Load balancing`: the load balancing algorithm used\n* `Targets`: the list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n* `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is `/api/foo`, then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n\n### URL Patterns\n\n* `Make service a 'public ui'`: add a default pattern as public routes\n* `Make service a 'private api'`: add a default pattern as private routes\n* `Public patterns`: by default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use `/.*`\n* `Private patterns`: if you define a public pattern that is a little bit too much, you can make some of public URL private again\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Otoroshi exchange protocol\n\n* `Enabled`: when enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside.\n* `Send challenge`: when disbaled, Otoroshi will not check if target service respond with sent random value.\n* `Send info. token`: when enabled, Otoroshi add an additional header containing current call informations\n* `Challenge token version`: version the otoroshi exchange protocol challenge. This option will be set to V2 in a near future.\n* `Info. token version`: version the otoroshi exchange protocol info token. This option will be set to Latest in a near future.\n* `Tokens TTL`: the number of seconds for tokens (state and info) lifes\n* `State token header name`: the name of the header containing the state token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.state)\n* `State token response header name`: the name of the header containing the state response token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.stateresp)\n* `Info token header name`: the name of the header containing the info token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.claim)\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Use same algo.`: when enabled, all JWT token in this section will use the same signing algorithm. If `use same algo.` is disabled, three more options will be displayed to select an algorithm for each step of the calls :\n * Otoroshi to backend\n * Backend to otoroshi\n * Info. token\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n### Authentication\n\n* `Enforce user authentication`: when enabled, user will be allowed to use the service (UI) only if they are registered users of the chosen authentication module.\n* `Auth. config`: authentication module used to protect the service\n* `Create a new auth config.`: navigate to the creation of authentication module page\n* `all auth config.`: navigate to the authentication pages\n\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Strict mode`: strict mode enabled\n\n### Api keys constraints\n\n* `From basic auth.`: you can pass the api key in Authorization header (ie. from 'Authorization: Basic xxx' header)\n* `Allow client id only usage`: you can pass the api key using client id only (ie. from Otoroshi-Token header)\n* `From custom headers`: you can pass the api key using custom headers (ie. Otoroshi-Client-Id and Otoroshi-Client-Secret headers)\n* `From JWT token`: you can pass the api key using a JWT token (ie. from 'Authorization: Bearer xxx' header)\n\n#### Basic auth. Api Key\n\n* `Custom header name`: the name of the header to get Authorization\n* `Custom query param name`: the name of the query param to get Authorization\n\n#### Client ID only Api Key\n\n* `Custom header name`: the name of the header to get the client id\n* `Custom query param name`: the name of the query param to get the client id\n\n#### Custom headers Api Key\n\n* `Custom client id header name`: the name of the header to get the client id\n* `Custom client secret header name`: the name of the header to get the client secret\n\n#### JWT Token Api Key\n\n* `Secret signed`: JWT can be signed by apikey secret using HMAC algo.\n* `Keypair signed`: JWT can be signed by an otoroshi managed keypair using RSA/EC algo.\n* `Include Http request attrs.`: if enabled, you have to put the following fields in the JWT token corresponding to the current http call (httpPath, httpVerb, httpHost)\n* `Max accepted token lifetime`: the maximum number of second accepted as token lifespan\n* `Custom header name`: the name of the header to get the jwt token\n* `Custom query param name`: the name of the query param to get the jwt token\n* `Custom cookie name`: the name of the cookie to get the jwt token\n\n### Routing constraints\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata\n\n### CORS support\n\n* `Enabled`: if enabled, CORS header will be check for each incoming request\n* `Allow credentials`: if enabled, the credentials will be sent. Credentials are cookies, authorization headers, or TLS client certificates.\n* `Allow origin`: if enabled, it will indicates whether the response can be shared with requesting code from the given\n* `Max age`: response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.\n* `Expose headers`: response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.\n* `Allow headers`: response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.\n* `Allow methods`: response header specifies one or more methods allowed when accessing a resource in response to a preflight request.\n* `Excluded patterns`: by default, when cors is enabled, everything has cors. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n#### Related documentations\n\n* @link[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) { open=new }\n* @link[Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) { open=new }\n* @link[Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) { open=new }\n* @link[Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) { open=new }\n* @link[Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) { open=new }\n\n### JWT tokens verification\n\n* `Verifiers`: list of selected verifiers to apply on the service\n* `Enabled`: if enabled, Otoroshi will enabled each verifier of the previous list\n* `Excluded patterns`: list of routes where the verifiers will not be apply\n\n### Pre Routing\n\nThis part has been deprecated and moved to the plugin section.\n\n### Access validation\nThis part has been deprecated and moved to the plugin section.\n\n### Gzip support\n\n* `Mimetypes allowed list`: gzip only the files that are matching to a format in the list\n* `Mimetypes blocklist`: will not gzip files matching to a format in the list. A possible way is to allowed all format by default by setting a `*` on the `Mimetypes allowed list` and to add the unwanted format in this list.\n* `Compression level`: the compression level where 9 gives us maximum compression but at the slowest speed. The default compression level is 5 and is a good compromise between speed and compression ratio.\n* `Buffer size`: chunking up a stream of bytes into limited size\n* `Chunk threshold`: if the content type of a request reached over the threshold, the response will be chunked\n* `Excluded patterns`: by default, when gzip is enabled, everything has gzip. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n### Client settings\n\n* `Use circuit breaker`: use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !\n* `Cache connections`: use a cache at host connection level to avoid reconnection time\n* `Client attempts`: specify how many times the client will retry to fetch the result of the request after an error before giving up.\n* `Client call timeout`: specify how long each call should last at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n* `C.breaker max errors`: specify how many errors can pass before opening the circuit breaker\n* `C.breaker retry delay`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor\n* `C.breaker backoff factor`: specify the factor to multiply the delay for each retry\n* `C.breaker window`: specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted\n\n#### Custom timeout settings (list)\n\n* `Path`: the path on which the timeout will be active\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Call timeout`: Specify how long each call should last at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n#### Proxy settings\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n### HTTP Headers\n\n* `Additional Headers In`: specify headers that will be added to each client request (from Otoroshi to target). Useful to add authentication.\n* `Additional Headers Out`: specify headers that will be added to each client response (from Otoroshi to client).\n* `Missing only Headers In`: specify headers that will be added to each client request (from Otoroshi to target) if not in the original request.\n* `Missing only Headers Out`: specify headers that will be added to each client response (from Otoroshi to client) if not in the original response.\n* `Remove incoming headers`: remove headers in the client request (from client to Otoroshi).\n* `Remove outgoing headers`: remove headers in the client response (from Otoroshi to client).\n* `Security headers`:\n* `Utility headers`:\n* `Matching Headers`: specify headers that MUST be present on client request to route it (pre routing). Useful to implement versioning.\n* `Headers verification`: verify that some headers has a specific value (post routing)\n\n### Additional settings \n\n* `OpenAPI`: specify an open API descriptor. Useful to display the documentation\n* `Tags`: specify tags for the service\n* `Metadata`: specify metadata for the service. Useful for analytics\n* `IP allowed list`: IP address that can access the service\n* `IP blocklist`: IP address that cannot access the service\n\n### Canary mode\n\n* `Enabled`: Canary mode enabled\n* `Traffic split`: Ratio of traffic that will be sent to canary targets. For instance, if traffic is at 0.2, for 10 request, 2 request will go on canary targets and 8 will go on regular targets.\n* `Targets`: The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n * `Target`:\n * `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n* `Campaign stats`:\n* `Use canary targets as standard targets`:\n\n### Healthcheck settings\n\n* `HealthCheck enabled`: to help failing fast, you can activate healthcheck on a specific URL.\n* `HealthCheck url`: the URL to check. Should return an HTTP 200 response. You can also respond with an 'Opun-Health-Check-Logic-Test-Result' header set to the value of the 'Opun-Health-Check-Logic-Test' request header + 42. to make the healthcheck complete.\n\n### Fault injection\n\n* `User facing app.`: if service is set as user facing, Snow Monkey can be configured to not being allowed to create outage on them.\n* `Chaos enabled`: activate or deactivate chaos setting on this service descriptor.\n\n### Custom errors template\n\n* `40x template`: html template displayed when 40x error occurred\n* `50x template`: html template displayed when 50x error occurred\n* `Build mode template`: html template displayed when the build mode is enabled\n* `Maintenance mode template`: html template displayed when the maintenance mode is enabled\n* `Custom messages`: override error message one by one\n\n### Request transformation\n\nThis part has been deprecated and moved to the plugin section.\n\n### Plugins\n\n* `Plugins`:\n \n * `Inject default config`: injects, if present, the default configuration of a selected plugin in the configuration object\n * `Documentation`: link to the documentation website of the plugin\n * `show/hide config. panel`: shows and hides the plugin panel which contains the plugin description and configuration\n* `Excluded patterns`: by default, when plugins are enabled, everything pass in. But sometimes you need to exclude something, so just add regex to matching path you want to exlude.\n* `Configuration`: the configuration of each enabled plugin, split by names and grouped in the same configuration object."},{"name":"service-groups.md","id":"/entities/service-groups.md","url":"/entities/service-groups.html","title":"Service groups","content":"# Service groups\n\nA service group is composed of an unique `id`, a `Group name`, a `Group description`, an `Organization` and a `Team`. As all Otoroshi resources, a service group have a list of tags and metadata associated.\n\n@@@ div { .centered-img }\n\n@@@\n\nThe first instinctive usage of service group is to group a list of services. \n\nWhen it's done, you can authorize an api key on a specific group. Instead of authorize an api key for each service, you can regroup a list of services together, and give authorization on the group (read the page on the api keys and the usage of the `Authorized on.` field).\n\n## Access to the list of service groups\n\nTo visualize and edit the list of groups, you can navigate to your instance on the `https://otoroshi.xxxxx/bo/dashboard/groups` route or click on the cog icon and select the Service groups button.\n\nOnce on the page, you can create a new item, edit an existing service group or delete an existing one.\n\n> When a service group is deleted, the resources associated are not deleted. On the other hand, the service group of associated resources is let empty.\n\n"},{"name":"tcp-services.md","id":"/entities/tcp-services.md","url":"/entities/tcp-services.html","title":"TCP services","content":"# TCP services\n\nTCP service are special kind of otoroshi services meant to proxy pure TCP connections (ssh, database, http, etc)\n\n## Global information\n\n* `Id`: generated unique identifier\n* `TCP service name`: the name of your TCP service\n* `Enabled`: enable and disable the service\n* `TCP service port`: the listening port\n* `TCP service interface`: network interface listen by the service\n* `Tags`: list of tags associated to the service\n* `Metadata`: list of metadata associated to the service\n\n## TLS\n\nthis section controls the TLS exposition of the service\n\n* `TLS mode`\n * `Disabled`: no TLS\n * `PassThrough`: as the target exposes TLS, the call will pass through otoroshi and use target TLS\n * `Enabled`: the service will be exposed using TLS and will chose certificate based on SNI\n* `Client Auth.`\n * `None` no mTLS needed to pass\n * `Want` pass with or without mTLS\n * `Need` need mTLS to pass\n\n## Server Name Indication (SNI)\n\nthis section control how SNI should be treated\n\n* `SNI routing enabled`: if enabled, the server will use the SNI hostname to determine which certificate to present to the client\n* `Forward to target if no SNI match`: if enabled, a call without any SNI match will be forward to the target\n* `Target host`: host of the target called if no SNI\n* `Target ip address`: ip of the target called if no SNI\n* `Target port`: port of the target called if no SNI\n* `TLS call`: encrypt the communication with TLS\n\n## Rules\n\nfor any listening TCP proxy, it is possible to route to multiple targets based on SNI or extracted http host (if proxying http)\n\n* `Matching domain name`: regex used to filter the list of domains where the rule will be applied\n* `Target host`: host of the target\n* `Target ip address`: ip of the target\n* `Target port`: port of the target\n* `TLS call`: enable this flag if the target is exposed using TLS\n"},{"name":"teams.md","id":"/entities/teams.md","url":"/entities/teams.html","title":"Teams","content":"# Teams\n\nIn Otoroshi, all resources are attached to an `Organization` and a `Team`. \n\nA team is composed of an unique `id`, a `name`, a `description` and an `Organization`. As all Otoroshi resources, a Team have a list of tags and metadata associated.\n\nA team have an unique organization and can be use on multiples resources (services, api keys, etc ...).\n\nA connected user on Otoroshi UI has a list of teams and organizations associated. It can be helpful when you want restrict the rights of a connected user.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of teams\n\nTo visualize and edit the list of teams, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/teams` route or click on the cog icon and select the teams button.\n\nOnce on the page, you can create a new item, edit an existing team or delete an existing one.\n\n> When a team is deleted, the resources associated are not deleted. On the other hand, the team of associated resources is let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of multiple teams in an organization\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"team-1\",\n \"team-2\"\n ]\n }\n ...\n}\n```\n\nor all teams\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"*\"\n ]\n }\n ...\n}\n```"},{"name":"features.md","id":"/features.md","url":"/features.html","title":"Features","content":"# Features\n\n**Traffic Management**\n\n* Can proxy any HTTP(s) service (apis, webapps, websocket, etc)\n* Can proxy any TCP service (app, database, etc)\n* Can proxy any GRPC service\n* Multiple load-balancing options: \n * RoundRobin\n * Random, Sticky\n * Ip address hash\n * Best Response Time\n* Distributed in-flight request limiting\t\n* Distributed rate limiting \n* End-to-end HTTP/1.1 support\n* End-to-end H2 support\n* End-to-end H3 support\n* Traffic mirroring\n* Traffic capture\n* Canary deployments\n* Relay routing \n* Tunnels for easier network exposition\n* Error templates\n\n**Routing**\n\n* Router can support ten of thousands of concurrent routes\n* Router support path params extraction (can be regex validated)\n* Routing based on \n * method\n * hostname (exact, wildcard)\n * path (exact, wildcard)\n * header values (exact, regex, wildcard)\n * query param values (exact, regex, wildcard)\n* Support full url rewriting\n\n**Routes customization**\n\n* Dozens of built-in middlewares (policies/plugins) \n * circuit breakers\n * automatic retries\n * buffering\n * gzip\n * headers manipulation\n * cors\n * body transformation\n * graphql gateway\n * etc \n* Support middlewares compiled to WASM (using extism)\n* Support Open Policy Agent policies for traffic control\n* Write your own custom middlewares\n * in scala deployed as jar files\n * in whatever language you want that can be compiled to WASM\n\n**Routes Monitoring**\n\n* Active healthchecks\n* Route state for the last 90 days\n* Calls tracing using W3C trace context\n* Export alerts and events to external database\n * file\n * S3\n * elastic\n * pulsar\n * kafka\n * webhook\n * mailer\n * logger\n* Real-time traffic metrics\n* Real-time traffic metrics (Datadog, Prometheus, StatsD)\n\n**Services discovery**\n\n* through DNS\n* through Eureka 2\n* through Kubernetes API\n* through custom otoroshi protocol\n\n**API security**\n\n* Access management with apikeys and quotas\n* Automatic apikeys secrets rotation\n* HTTPS and TLS\n* End-to-end mTLS calls \n* Routing constraints\n* Routing restrictions\n* JWT tokens validation and manipulation\n * can support multiple validator on the same routes\n\n**Administration UI**\n\n* Manage and organize all resources\n* Secured users access with Authentication module\n* Audited users actions\n* Dynamic changes at runtime without full reload\n* Test your routes without any external tools\n\n**Webapp authentication and security**\n\n* OAuth2.0/2.1 authentication\n* OpenID Connect (OIDC) authentication\n* LDAP authentication\n* JWT authentication\n* OAuth 1.0a authentication\n* SAML V2 authentication\n* Internal users management\n* Secret vaults support\n * Environment variables\n * Hashicorp Vault\n * Azure key vault\n * AWS secret manager\n * Google secret manager\n * Kubernetes secrets\n * Izanami\n * Spring Cloud Config\n * Http\n * Local\n\n**Certificates management**\n\n* Dynamic TLS certificates store \n* Dynamic TLS termination\n* Internal PKI\n * generate self signed certificates/CAs\n * generate/sign certificates/CAs/subCAs\n * AIA\n * OCSP responder\n * import P12/certificate bundles\n* ACME / Let's Encrypt support\n* On-the-fly certificate generation based on a CA certificate without request loss\n* JWKS exposition for public keypair\n* Default certificate\n* Customize mTLS trusted CAs in the TLS handshake\n\n**Clustering**\n\n* based on a control plane/data plane pattern\n* encrypted communication\n* backup capabilities to allow data plane to start without control plane reachable to improve resilience\n* relay routing to forward traffic from one network zone to others\n* distributed web authentication accross nodes\n\n**Performances and testing**\n\n* Chaos engineering\n* Horizontal Scalability or clustering\n* Canary testing\n* Http client in UI\n* Request debugging\n* Traffic capture\n\n**Kubernetes integration**\n\n* Standard Ingress controller\n* Custom Ingress controller\n * Manage Otoroshi resources from Kubernetes\n* Validation of resources via webhook\n* Service Mesh for easy service-to-service communication (based on Kubernetes sidecars)\n\n**Organize**\n\n* multi-organizations\n* multi-teams\n* routes groups\n\n**Developpers portal**\n\n* Using @link:[Daikoku](https://maif.github.io/daikoku/manual/index.html) { open=new }\n"},{"name":"getting-started.md","id":"/getting-started.md","url":"/getting-started.html","title":"Getting Started","content":"# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
\nRoute plugins:\nApikeys\n
\n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
\nRoute plugins:\nAuthentication\n
\n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial."},{"name":"communicate-with-kafka.md","id":"/how-to-s/communicate-with-kafka.md","url":"/how-to-s/communicate-with-kafka.html","title":"Communicate with Kafka","content":"# Communicate with Kafka\n\nEvery matching event can be sent to an [Apache Kafka topic](https://kafka.apache.org/).\n\n### SASL mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:SASL_PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n - KAFKA_CLIENT_USERS=user\n - KAFKA_CLIENT_PASSWORDS=password\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### PLAINTEXT mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SSL mechanism\n\n````bash\nwget https://raw.githubusercontent.com/confluentinc/confluent-platform-security-tools/master/kafka-generate-ssl.sh\n````\n\n````bash\nchmod +x kafka-generate-ssl.sh\n````\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"wurstmeister/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n\n kafka:\n image: wurstmeister/kafka:2.12-2.2.0\n depends_on:\n - zookeeper\n ports:\n - \"9092:9092\"\n environment:\n KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:9092'\n KAFKA_LISTENERS: 'SSL://0.0.0.0:9092'\n KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'\n KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_SSL_KEYSTORE_LOCATION: '/keystore/kafka.keystore.jks'\n KAFKA_SSL_KEYSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_KEY_PASSWORD: 'otoroshi'\n KAFKA_SSL_TRUSTSTORE_LOCATION: '/truststore/kafka.truststore.jks'\n KAFKA_SSL_TRUSTSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_SECURITY_INTER_BROKER_PROTOCOL: 'SSL'\n volumes:\n - ./truststore:/truststore\n - ./keystore:/keystore\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SASL_SSL mechanism\n\nGenerate the TLS certificates for the Kafka broker.\n\nCreate a file `generate.sh` with the following content and run the command\n\n````bash\nchmod +x generate.sh && ./generate.sh\n````\n\n````bash\n# Content of the generate.sh file\n\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚°\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚° cat generate.sh\n#!/usr/bin/env bash\n\nset -e\n\nKEYSTORE_FILENAME=\"kafka.keystore.jks\"\nVALIDITY_IN_DAYS=3650\nDEFAULT_TRUSTSTORE_FILENAME=\"kafka.truststore.jks\"\nTRUSTSTORE_WORKING_DIRECTORY=\"truststore\"\nKEYSTORE_WORKING_DIRECTORY=\"keystore\"\nCA_CERT_FILE=\"ca-cert\"\nKEYSTORE_SIGN_REQUEST=\"cert-file\"\nKEYSTORE_SIGN_REQUEST_SRL=\"ca-cert.srl\"\nKEYSTORE_SIGNED_CERT=\"cert-signed\"\n\nfunction file_exists_and_exit() {\n echo \"'$1' cannot exist. Move or delete it before\"\n echo \"re-running this script.\"\n exit 1\n}\n\nif [ -e \"$KEYSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $KEYSTORE_WORKING_DIRECTORY\nfi\n\nif [ -e \"$CA_CERT_FILE\" ]; then\n file_exists_and_exit $CA_CERT_FILE\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST_SRL\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST_SRL\nfi\n\nif [ -e \"$KEYSTORE_SIGNED_CERT\" ]; then\n file_exists_and_exit $KEYSTORE_SIGNED_CERT\nfi\n\necho\necho \"Welcome to the Kafka SSL keystore and truststore generator script.\"\n\necho\necho \"First, do you need to generate a trust store and associated private key,\"\necho \"or do you already have a trust store file and private key?\"\necho\necho -n \"Do you need to generate a trust store and associated private key? [yn] \"\nread generate_trust_store\n\ntrust_store_file=\"\"\ntrust_store_private_key_file=\"\"\n\nif [ \"$generate_trust_store\" == \"y\" ]; then\n if [ -e \"$TRUSTSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $TRUSTSTORE_WORKING_DIRECTORY\n fi\n\n mkdir $TRUSTSTORE_WORKING_DIRECTORY\n echo\n echo \"OK, we'll generate a trust store and associated private key.\"\n echo\n echo \"First, the private key.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - A password for the private key. Remember this.\"\n echo \" - Information about you and your company.\"\n echo \" - NOTE that the Common Name (CN) is currently not important.\"\n\n openssl req -new -x509 -keyout $TRUSTSTORE_WORKING_DIRECTORY/ca-key \\\n -out $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -days $VALIDITY_IN_DAYS\n\n trust_store_private_key_file=\"$TRUSTSTORE_WORKING_DIRECTORY/ca-key\"\n\n echo\n echo \"Two files were created:\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-key -- the private key used later to\"\n echo \" sign certificates\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -- the certificate that will be\"\n echo \" stored in the trust store in a moment and serve as the certificate\"\n echo \" authority (CA). Once this certificate has been stored in the trust\"\n echo \" store, it will be deleted. It can be retrieved from the trust store via:\"\n echo \" $ keytool -keystore -export -alias CARoot -rfc\"\n\n echo\n echo \"Now the trust store will be generated from the certificate.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - the trust store's password (labeled 'keystore'). Remember this\"\n echo \" - a confirmation that you want to import the certificate\"\n\n keytool -keystore $TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME \\\n -alias CARoot -import -file $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\n\n trust_store_file=\"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME\"\n\n echo\n echo \"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME was created.\"\n\n # don't need the cert because it's in the trust store.\n rm $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\nelse\n echo\n echo -n \"Enter the path of the trust store file. \"\n read -e trust_store_file\n\n if ! [ -f $trust_store_file ]; then\n echo \"$trust_store_file isn't a file. Exiting.\"\n exit 1\n fi\n\n echo -n \"Enter the path of the trust store's private key. \"\n read -e trust_store_private_key_file\n\n if ! [ -f $trust_store_private_key_file ]; then\n echo \"$trust_store_private_key_file isn't a file. Exiting.\"\n exit 1\n fi\nfi\n\necho\necho \"Continuing with:\"\necho \" - trust store file: $trust_store_file\"\necho \" - trust store private key: $trust_store_private_key_file\"\n\nmkdir $KEYSTORE_WORKING_DIRECTORY\n\necho\necho \"Now, a keystore will be generated. Each broker and logical client needs its own\"\necho \"keystore. This script will create only one keystore. Run this script multiple\"\necho \"times for multiple keystores.\"\necho\necho \"You will be prompted for the following:\"\necho \" - A keystore password. Remember it.\"\necho \" - Personal information, such as your name.\"\necho \" NOTE: currently in Kafka, the Common Name (CN) does not need to be the FQDN of\"\necho \" this host. However, at some point, this may change. As such, make the CN\"\necho \" the FQDN. Some operating systems call the CN prompt 'first / last name'\"\necho \" - A key password, for the key being generated within the keystore. Remember this.\"\n\n# To learn more about CNs and FQDNs, read:\n# https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedTrustManager.html\n\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME \\\n -alias localhost -validity $VALIDITY_IN_DAYS -genkey -keyalg RSA\n\necho\necho \"'$KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME' now contains a key pair and a\"\necho \"self-signed certificate. Again, this keystore can only be used for one broker or\"\necho \"one logical client. Other brokers or clients need to generate their own keystores.\"\n\necho\necho \"Fetching the certificate from the trust store and storing in $CA_CERT_FILE.\"\necho\necho \"You will be prompted for the trust store's password (labeled 'keystore')\"\n\nkeytool -keystore $trust_store_file -export -alias CARoot -rfc -file $CA_CERT_FILE\n\necho\necho \"Now a certificate signing request will be made to the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost \\\n -certreq -file $KEYSTORE_SIGN_REQUEST\n\necho\necho \"Now the trust store's private key (CA) will sign the keystore's certificate.\"\necho\necho \"You will be prompted for the trust store's private key password.\"\nopenssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \\\n -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \\\n -days $VALIDITY_IN_DAYS -CAcreateserial\n# creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed.\n\necho\necho \"Now the CA will be imported into the keystore.\"\necho\necho \"You will be prompted for the keystore's password and a confirmation that you want to\"\necho \"import the certificate.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias CARoot \\\n -import -file $CA_CERT_FILE\nrm $CA_CERT_FILE # delete the trust store cert because it's stored in the trust store.\n\necho\necho \"Now the keystore's signed certificate will be imported back into the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost -import \\\n -file $KEYSTORE_SIGNED_CERT\n\necho\necho \"All done!\"\necho\necho \"Delete intermediate files? They are:\"\necho \" - '$KEYSTORE_SIGN_REQUEST_SRL': CA serial number\"\necho \" - '$KEYSTORE_SIGN_REQUEST': the keystore's certificate signing request\"\necho \" (that was fulfilled)\"\necho \" - '$KEYSTORE_SIGNED_CERT': the keystore's certificate, signed by the CA, and stored back\"\necho \" into the keystore\"\necho -n \"Delete? [yn] \"\nread delete_intermediate_files\n\nif [ \"$delete_intermediate_files\" == \"y\" ]; then\n rm $KEYSTORE_SIGN_REQUEST_SRL\n rm $KEYSTORE_SIGN_REQUEST\n rm $KEYSTORE_SIGNED_CERT\nfi\n````\n\nCreate, in the same repository, a repository named `secrets` with the following configuration.\n\n````bash \n# Content of ~/tmp/kafka/secrets/kafka_server_jaas.conf\n\nClient {\n org.apache.kafka.common.security.plain.PlainLoginModule required\n username=\"user\"\n password=\"password\";\n};\n````\n\nCreate a `docker-compose.yml` file with the following content.\n\n````bash\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n````\n\nAt this point, your repository should be \n````\n/tmp/kafka\n | generate.sh\n | docker-compose.yml\n | truststore\n | kafka.truststore.jks\n | keystore \n | kafka.keystore.jks\n | secrets \n | kafka_server_jaas.conf\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"create-custom-auth-module.md","id":"/how-to-s/create-custom-auth-module.md","url":"/how-to-s/create-custom-auth-module.html","title":"Create your Authentication module","content":"# Create your Authentication module\n\nAuthentication modules can be used to protect routes. In some cases, you need to create your own custom authentication module to create a new one or simply inherit and extend an exiting module.\n\nYou can write your own authentication using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"my-custom-auth-module\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1x.x.x\"\n )\n```\n\nJust below, you can find an example of Custom Auth. module. \n\n```scala\npackage auth.custom\n\nimport akka.http.scaladsl.util.FastFuture\nimport otoroshi.auth.{AuthModule, AuthModuleConfig, Form, SessionCookieValues}\nimport otoroshi.controllers.routes\nimport otoroshi.env.Env\nimport otoroshi.models._\nimport otoroshi.security.IdGenerator\nimport otoroshi.utils.JsonPathValidator\nimport otoroshi.utils.syntax.implicits.BetterSyntax\nimport play.api.http.MimeTypes\nimport play.api.libs.json._\nimport play.api.mvc._\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\ncase class CustomModuleConfig(\n id: String,\n name: String,\n desc: String,\n clientSideSessionEnabled: Boolean,\n sessionMaxAge: Int = 86400,\n userValidators: Seq[JsonPathValidator] = Seq.empty,\n tags: Seq[String],\n metadata: Map[String, String],\n sessionCookieValues: SessionCookieValues,\n location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),\n form: Option[Form] = None,\n foo: String = \"bar\"\n ) extends AuthModuleConfig {\n def `type`: String = \"custom\"\n def humanName: String = \"Custom Authentication\"\n\n override def authModule(config: GlobalConfig): AuthModule = CustomAuthModule(this)\n override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)\n\n lazy val format = new Format[CustomModuleConfig] {\n override def writes(o: CustomModuleConfig): JsValue = o.asJson\n\n override def reads(json: JsValue): JsResult[CustomModuleConfig] = Try {\n CustomModuleConfig(\n location = otoroshi.models.EntityLocation.readFromKey(json),\n id = (json \\ \"id\").as[String],\n name = (json \\ \"name\").as[String],\n desc = (json \\ \"desc\").asOpt[String].getOrElse(\"--\"),\n clientSideSessionEnabled = (json \\ \"clientSideSessionEnabled\").asOpt[Boolean].getOrElse(true),\n sessionMaxAge = (json \\ \"sessionMaxAge\").asOpt[Int].getOrElse(86400),\n metadata = (json \\ \"metadata\").asOpt[Map[String, String]].getOrElse(Map.empty),\n tags = (json \\ \"tags\").asOpt[Seq[String]].getOrElse(Seq.empty[String]),\n sessionCookieValues =\n (json \\ \"sessionCookieValues\").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),\n userValidators = (json \\ \"userValidators\")\n .asOpt[Seq[JsValue]]\n .map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))\n .getOrElse(Seq.empty),\n form = (json \\ \"form\").asOpt[JsValue].flatMap(json => Form._fmt.reads(json) match {\n case JsSuccess(value, _) => Some(value)\n case JsError(_) => None\n }),\n foo = (json \\ \"foo\").asOpt[String].getOrElse(\"bar\")\n )\n } match {\n case Failure(exception) => JsError(exception.getMessage)\n case Success(value) => JsSuccess(value)\n }\n }.asInstanceOf[Format[AuthModuleConfig]]\n\n override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = format\n\n override def asJson =\n location.jsonWithKey ++ Json.obj(\n \"type\" -> \"custom\",\n \"id\" -> this.id,\n \"name\" -> this.name,\n \"desc\" -> this.desc,\n \"clientSideSessionEnabled\" -> this.clientSideSessionEnabled,\n \"sessionMaxAge\" -> this.sessionMaxAge,\n \"metadata\" -> this.metadata,\n \"tags\" -> JsArray(tags.map(JsString.apply)),\n \"sessionCookieValues\" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),\n \"userValidators\" -> JsArray(userValidators.map(_.json)),\n \"form\" -> this.form.map(Form._fmt.writes),\n \"foo\" -> foo\n )\n\n def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)\n\n override def cookieSuffix(desc: ServiceDescriptor) = s\"custom-auth-$id\"\n def theDescription: String = desc\n def theMetadata: Map[String, String] = metadata\n def theName: String = name\n def theTags: Seq[String] = tags\n}\n\nobject CustomAuthModule {\n def defaultConfig = CustomModuleConfig(\n id = IdGenerator.namedId(\"auth_mod\", IdGenerator.uuid),\n name = \"My custom auth. module\",\n desc = \"My custom auth. module\",\n tags = Seq.empty,\n metadata = Map.empty,\n sessionCookieValues = SessionCookieValues(),\n clientSideSessionEnabled = true,\n form = None)\n}\n\ncase class CustomAuthModule(authConfig: CustomModuleConfig) extends AuthModule {\n def this() = this(CustomAuthModule.defaultConfig)\n\n override def paLoginPage(request: RequestHeader, config: GlobalConfig, descriptor: ServiceDescriptor, isRoute: Boolean)\n (implicit ec: ExecutionContext, env: Env): Future[Result] = {\n val redirect = request.getQueryString(\"redirect\")\n val hash = env.sign(s\"${authConfig.id}:::${descriptor.id}\")\n env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>\n Results\n .Ok(auth.custom.views.html.login(s\"/privateapps/generic/callback?desc=${descriptor.id}&hash=$hash&route=${isRoute}\", token))\n .as(MimeTypes.HTML)\n .addingToSession(\n \"ref\" -> authConfig.id,\n s\"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}\" -> redirect.getOrElse(\n routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)(request)\n )\n )(request)\n .future\n }\n }\n\n override def paLogout(request: RequestHeader, user: Option[PrivateAppsUser], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = FastFuture.successful(Right(None))\n\n override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[String, PrivateAppsUser]] = {\n PrivateAppsUser(\n randomId = IdGenerator.token(64),\n name = \"foo\",\n email = s\"foo@oto.tools\",\n profile = Json.obj(\n \"name\" -> \"foo\",\n \"email\" -> s\"foo@oto.tools\"\n ),\n realm = authConfig.cookieSuffix(descriptor),\n otoroshiData = None,\n authConfigId = authConfig.id,\n tags = Seq.empty,\n metadata = Map.empty,\n location = authConfig.location\n )\n .validate(authConfig.userValidators)\n .vfuture\n }\n\n override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Result] = ???\n\n override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = ???\n\n override def boCallback(request: Request[AnyContent], config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[String, BackOfficeUser]] = ???\n}\n```\n\nThis custom Auth. module inherits from AuthModule (the Auth module have to inherit from the AuthModule trait to be found by Otoroshi). It exposes a simple UI to login, and create an user for each callback request without any verification. Methods starting with bo will be called in case that the auth. module is used on the back office and in other cases, the pa methods (pa for Private App) will be called to protect a route.\n\nThis custom Auth. module uses a [Play template](https://www.playframework.com/documentation/2.8.x/ScalaTemplates) to display the login page. It's not required by Otoroshi but it's a easy way to create a login form.\n\n```html \n@import otoroshi.env.Env\n\n@(action: String, token: String)\n\n
\n

Login page

\n\n
\n \n \n Login\n \n \n
\n```\n\nYour hierarchy files should be something like:\n\n```\nauth\n| custom\n |customModule.scala\n | views\n | login.scala.html\n```\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/customModule.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in the authentication modules, you can chose your custom module in the list."},{"name":"custom-initial-state.md","id":"/how-to-s/custom-initial-state.md","url":"/how-to-s/custom-initial-state.html","title":"Initial state customization","content":"# Initial state customization\n\nwhen you start otoroshi for the first time, some basic entities will be created and stored in the datastore in order to make your instance work properly. However it might not be enough for your use case but you do want to bother with restoring a complete otoroshi export.\n\nIn order to make state customization easy, otoroshi provides the config. key `otoroshi.initialCustomization`, overriden by the env. variable `OTOROSHI_INITIAL_CUSTOMIZATION`\n\nThe expected structure is the following :\n\n```javascript\n{\n \"config\": { ... },\n \"admins\": [],\n \"simpleAdmins\": [],\n \"serviceGroups\": [],\n \"apiKeys\": [],\n \"serviceDescriptors\": [],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [],\n \"teams\": []\n}\n```\n\nin this structure, everything is optional. For every array property, items will be added to the datastore. For the global config. object, you can just add the parts that you need, and they will be merged with the existing config. object of the datastore.\n\n## Customize the global config.\n\nfor instance, if you want to customize the behavior of the TLS termination, you can use the following :\n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"config\":{\"tlsSettings\":{\"defaultDomain\":\"www.foo.bar\",\"randomIfNotFound\":false}}'\n```\n\n## Customize entities\n\nif you want to add apikeys at first boot \n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"apikeys\":[{\"_loc\":{\"tenant\":\"default\",\"teams\":[\"default\"]},\"clientId\":\"ksVlQ2KlZm0CnDfP\",\"clientSecret\":\"usZYbE1iwSsbpKY45W8kdbZySj1M5CWvFXe0sPbZ0glw6JalMsgorDvSBdr2ZVBk\",\"clientName\":\"awesome-apikey\",\"description\":\"the awesome apikey\",\"authorizedGroup\":\"default\",\"authorizedEntities\":[\"group_default\"],\"enabled\":true,\"readOnly\":false,\"allowClientIdOnly\":false,\"throttlingQuota\":10000000,\"dailyQuota\":10000000,\"monthlyQuota\":10000000,\"constrainedServicesOnly\":false,\"restrictions\":{\"enabled\":false,\"allowLast\":true,\"allowed\":[],\"forbidden\":[],\"notFound\":[]},\"rotation\":{\"enabled\":false,\"rotationEvery\":744,\"gracePeriod\":168,\"nextSecret\":null},\"validUntil\":null,\"tags\":[],\"metadata\":{}}]}'\n```\n"},{"name":"custom-log-levels.md","id":"/how-to-s/custom-log-levels.md","url":"/how-to-s/custom-log-levels.html","title":"Log levels customization","content":"# Log levels customization\n\nIf you want to customize the log level of your otoroshi instances, it's pretty easy to do it using environment variables or configuration file.\n\n## Customize log level for one logger with configuration file\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare in your otoroshi configuration file\n\n```\notoroshi.loggers {\n ...\n otoroshi-http-handler = \"DEBUG\"\n ...\n}\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## Customize log level for one logger with environment variable\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare an environment variable named `OTOROSHI_LOGGERS_OTOROSHI_HTTP_HANDLER` with value `DEBUG`. The rule is \n\n```scala\n\"OTOROSHI_LOGGERS_\" + loggerName.toUpperCase().replace(\"-\", \"_\")\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## List of loggers\n\n* [`otoroshi-error-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-handler%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-http-handler-debug`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler-debug%22%29)\n* [`otoroshi-websocket-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler%22%29)\n* [`otoroshi-websocket`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket%22%29)\n* [`otoroshi-websocket-handler-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler-actor%22%29)\n* [`otoroshi-snowmonkey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-worker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-worker%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-auth-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-controller%22%29)\n* [`otoroshi-swagger-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-swagger-controller%22%29)\n* [`otoroshi-u2f-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-u2f-controller%22%29)\n* [`otoroshi-backoffice-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-backoffice-api%22%29)\n* [`otoroshi-health-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-api%22%29)\n* [`otoroshi-stats-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-stats-api%22%29)\n* [`otoroshi-admin-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-admin-api%22%29)\n* [`otoroshi-auth-modules-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-modules-api%22%29)\n* [`otoroshi-certificates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-api%22%29)\n* [`otoroshi-pki`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-pki%22%29)\n* [`otoroshi-scripts-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-scripts-api%22%29)\n* [`otoroshi-analytics-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-api%22%29)\n* [`otoroshi-import-export-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-import-export-api%22%29)\n* [`otoroshi-templates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-templates-api%22%29)\n* [`otoroshi-teams-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-teams-api%22%29)\n* [`otoroshi-events-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-api%22%29)\n* [`otoroshi-canary-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary-api%22%29)\n* [`otoroshi-data-exporter-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-api%22%29)\n* [`otoroshi-services-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-services-api%22%29)\n* [`otoroshi-tcp-service-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-service-api%22%29)\n* [`otoroshi-tenants-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tenants-api%22%29)\n* [`otoroshi-global-config-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config-api%22%29)\n* [`otoroshi-apikeys-fs-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fs-api%22%29)\n* [`otoroshi-apikeys-fg-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fg-api%22%29)\n* [`otoroshi-apikeys-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-api%22%29)\n* [`otoroshi-statsd-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-statsd-actor%22%29)\n* [`otoroshi-snow-monkey-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snow-monkey-api%22%29)\n* [`otoroshi-jobs-eventstore-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-eventstore-checker%22%29)\n* [`otoroshi-initials-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-initials-certs-job%22%29)\n* [`otoroshi-alert-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor%22%29)\n* [`otoroshi-alert-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor-supervizer%22%29)\n* [`otoroshi-alerts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alerts%22%29)\n* [`otoroshi-apikeys-secrets-rotation-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-secrets-rotation-job%22%29)\n* [`otoroshi-loader`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-loader%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-analytics-writes-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-writes-elastic%22%29)\n* [`otoroshi-analytics-reads-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-reads-elastic%22%29)\n* [`otoroshi-events-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-actor-supervizer%22%29)\n* [`otoroshi-data-exporter`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter%22%29)\n* [`otoroshi-data-exporter-update-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-update-job%22%29)\n* [`otoroshi-kafka-wrapper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-wrapper%22%29)\n* [`otoroshi-kafka-connector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-connector%22%29)\n* [`otoroshi-analytics-webhook`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-webhook%22%29)\n* [`otoroshi-jobs-software-updates`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-software-updates%22%29)\n* [`otoroshi-analytics-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor%22%29)\n* [`otoroshi-analytics-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor-supervizer%22%29)\n* [`otoroshi-analytics-event`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-event%22%29)\n* [`otoroshi-env`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-env%22%29)\n* [`otoroshi-script-compiler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-compiler%22%29)\n* [`otoroshi-script-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-manager%22%29)\n* [`otoroshi-script`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-custom-timeouts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-custom-timeouts%22%29)\n* [`otoroshi-client-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-config%22%29)\n* [`otoroshi-canary`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary%22%29)\n* [`otoroshi-redirection-settings`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redirection-settings%22%29)\n* [`otoroshi-service-descriptor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor%22%29)\n* [`otoroshi-service-descriptor-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor-datastore%22%29)\n* [`otoroshi-console-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-console-mailer%22%29)\n* [`otoroshi-mailgun-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailgun-mailer%22%29)\n* [`otoroshi-mailjet-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailjet-mailer%22%29)\n* [`otoroshi-sendgrid-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sendgrid-mailer%22%29)\n* [`otoroshi-generic-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-generic-mailer%22%29)\n* [`otoroshi-clevercloud-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-clevercloud-client%22%29)\n* [`otoroshi-metrics`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-metrics%22%29)\n* [`otoroshi-gzip-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-gzip-config%22%29)\n* [`otoroshi-regex-pool`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-regex-pool%22%29)\n* [`otoroshi-ws-client-chooser`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ws-client-chooser%22%29)\n* [`otoroshi-akka-ws-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-akka-ws-client%22%29)\n* [`otoroshi-http-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-implicits%22%29)\n* [`otoroshi-service-group`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-group%22%29)\n* [`otoroshi-data-exporter-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config%22%29)\n* [`otoroshi-data-exporter-config-migration-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config-migration-job%22%29)\n* [`otoroshi-lets-encrypt-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lets-encrypt-helper%22%29)\n* [`otoroshi-apkikey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apkikey%22%29)\n* [`otoroshi-error-template`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-template%22%29)\n* [`otoroshi-job-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-job-manager%22%29)\n* [`otoroshi-plugins-internal-eventlistener-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-internal-eventlistener-actor%22%29)\n* [`otoroshi-global-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config%22%29)\n* [`otoroshi-jwks`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwks%22%29)\n* [`otoroshi-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwt-verifier%22%29)\n* [`otoroshi-global-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-jwt-verifier%22%29)\n* [`otoroshi-snowmonkey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey-config%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-service-datatstore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-datatstore%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-redis-like-store`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-like-store%22%29)\n* [`otoroshi-globalconfig-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-globalconfig-datastore%22%29)\n* [`otoroshi-reactive-pg-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-datastores%22%29)\n* [`otoroshi-reactive-pg-kv`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-kv%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-apikey-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikey-datastore%22%29)\n* [`otoroshi-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastore%22%29)\n* [`otoroshi-certificate-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificate-datastore%22%29)\n* [`otoroshi-simple-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-simple-admin-datastore%22%29)\n* [`otoroshi-atomic-in-memory-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-atomic-in-memory-datastore%22%29)\n* [`otoroshi-lettuce-redis`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis%22%29)\n* [`otoroshi-lettuce-redis-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis-cluster%22%29)\n* [`otoroshi-redis-lettuce-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-lettuce-datastores%22%29)\n* [`otoroshi-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastores%22%29)\n* [`otoroshi-file-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-file-db-datastores%22%29)\n* [`otoroshi-http-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-db-datastores%22%29)\n* [`otoroshi-s3-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-s3-datastores%22%29)\n* [`PluginDocumentationGenerator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22PluginDocumentationGenerator%22%29)\n* [`otoroshi-health-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-checker%22%29)\n* [`otoroshi-healthcheck-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-job%22%29)\n* [`otoroshi-healthcheck-local-cache-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-local-cache-job%22%29)\n* [`otoroshi-plugins-response-cache`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-response-cache%22%29)\n* [`otoroshi-oidc-apikey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-oidc-apikey-config%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-info%22%29)\n* [`otoroshi-plugins-ipstack-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-ipstack-geolocation-info%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-helper%22%29)\n* [`otoroshi-plugins-user-agent-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-helper%22%29)\n* [`otoroshi-plugins-user-agent-extractor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-extractor%22%29)\n* [`otoroshi-global-el`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-el%22%29)\n* [`otoroshi-plugins-oauth1-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-oauth1-caller-plugin%22%29)\n* [`otoroshi-dynamic-sslcontext`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-dynamic-sslcontext%22%29)\n* [`otoroshi-plugins-access-log-clf`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-clf%22%29)\n* [`otoroshi-plugins-access-log-json`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-json%22%29)\n* [`otoroshi-plugins-kafka-access-log`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kafka-access-log%22%29)\n* [`otoroshi-plugins-kubernetes-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-client%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-sync%22%29)\n* [`otoroshi-plugins-kubernetes-crds-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-crds-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-sync%22%29)\n* [`otoroshi-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cluster%22%29)\n* [`otoroshi-crd-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-crd-validator%22%29)\n* [`otoroshi-sidecar-injector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sidecar-injector%22%29)\n* [`otoroshi-plugins-kubernetes-cert-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-cert-sync%22%29)\n* [`otoroshi-plugins-kubernetes-to-otoroshi-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-to-otoroshi-certs-job%22%29)\n* [`otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job%22%29)\n* [`otoroshi-apikeys-workflow-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-workflow-job%22%29)\n* [`otoroshi-cert-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-helper%22%29)\n* [`otoroshi-certificates-ocsp`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-ocsp%22%29)\n* [`otoroshi-claim`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-claim%22%29)\n* [`otoroshi-cert`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert%22%29)\n* [`otoroshi-ssl-provider`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-provider%22%29)\n* [`otoroshi-cert-data`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-data%22%29)\n* [`otoroshi-client-cert-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-cert-validator%22%29)\n* [`otoroshi-ssl-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-implicits%22%29)\n* [`otoroshi-saml-validator-utils`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-saml-validator-utils%22%29)\n* [`otoroshi-global-saml-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-saml-config%22%29)\n* [`otoroshi-plugins-hmac-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-caller-plugin%22%29)\n* [`otoroshi-plugins-hmac-access-validator-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-access-validator-plugin%22%29)\n* [`otoroshi-plugins-hasallowedusersvalidator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hasallowedusersvalidator%22%29)\n* [`otoroshi-auth-module-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-module-config%22%29)\n* [`otoroshi-basic-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-basic-auth-config%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n* [`otoroshi-plugins-jsonpath-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-jsonpath-helper%22%29)\n* [`otoroshi-global-oauth2-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-config%22%29)\n* [`otoroshi-global-oauth2-module`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-module%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n"},{"name":"end-to-end-mtls.md","id":"/how-to-s/end-to-end-mtls.md","url":"/how-to-s/end-to-end-mtls.html","title":"End-to-end mTLS","content":"# End-to-end mTLS\n\nIf you want to use MTLS on otoroshi, you first need to enable it. It is not enabled by default as it will make TLS handshake way heavier. \nTo enable it just change the following config :\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\nYou can use the `Want` setup if you cant to have both mtls on some services and no mtls on other services.\n\nYou can also change the trusted CA list sent in the handshake certificate request from the `Danger Zone` in `Tls Settings`.\n\nOtoroshi support mutual TLS out of the box. mTLS from client to Otoroshi and from Otoroshi to targets are supported. In this article we will see how to configure Otoroshi to use end-to-end mTLS. All code and files used in this articles can be found on the [Otoroshi github](https://github.com/MAIF/otoroshi/tree/master/demos/mtls)\n\n### Create certificates\n\nBut first we need to generate some certificates to make the demo work\n\n```sh\nmkdir mtls-demo\ncd mtls-demo\nmkdir ca\nmkdir server\nmkdir client\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-backend.key 4096\n# remove pass phrase\nopenssl rsa -in ./ca/ca-backend.key -out ./ca/ca-backend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-backend.key -out ./ca/ca-backend.cer -subj \"/CN=MTLSB\"\n\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-frontend.key 2048\n# remove pass phrase\nopenssl rsa -in ./ca/ca-frontend.key -out ./ca/ca-frontend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-frontend.key -out ./ca/ca-frontend.cer -subj \"/CN=MTLSF\"\n\n\n# now create the backend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.backend.oto.tools.key -out ./server/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.backend.oto.tools.key -sha256 -out ./server/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 1 -out ./server/_.backend.oto.tools.cer\n# verify the certificate, should output './server/_.backend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-backend.cer ./server/_.backend.oto.tools.cer\n\n\n# now create the frontend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.frontend.oto.tools.key -out ./server/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.frontend.oto.tools.key -sha256 -out ./server/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 1 -out ./server/_.frontend.oto.tools.cer\n# verify the certificate, should output './server/_.frontend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.cer\n\n\n# now create the client cert key for backend, use password as pass phrase\nopenssl genrsa -out ./client/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 2 -out ./client/_.backend.oto.tools.cer\n# generate a pem version of the cert and key, use password as password\nopenssl x509 -in client/_.backend.oto.tools.cer -out client/_.backend.oto.tools.pem -outform PEM\n\n\n# now create the client cert key for frontend, use password as pass phrase\nopenssl genrsa -out ./client/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 2 -out ./client/_.frontend.oto.tools.cer\n# generate a pkcs12 version of the cert and key, use password as password\n# openssl pkcs12 -export -clcerts -in client/_.frontend.oto.tools.cer -inkey client/_.frontend.oto.tools.key -out client/_.frontend.oto.tools.p12\nopenssl x509 -in client/_.frontend.oto.tools.cer -out client/_.frontend.oto.tools.pem -outform PEM\n```\n\nOnce it's done, you should have something like\n\n```sh\n$ tree\n.\n├── backend.js\n├── ca\n│   ├── ca-backend.cer\n│   ├── ca-backend.key\n│   ├── ca-frontend.cer\n│   └── ca-frontend.key\n├── client\n│   ├── _.backend.oto.tools.cer\n│   ├── _.backend.oto.tools.csr\n│   ├── _.backend.oto.tools.key\n│   ├── _.backend.oto.tools.pem\n│   ├── _.frontend.oto.tools.cer\n│   ├── _.frontend.oto.tools.csr\n│   ├── _.frontend.oto.tools.key\n│   └── _.frontend.oto.tools.pem\n└── server\n ├── _.backend.oto.tools.cer\n ├── _.backend.oto.tools.csr\n ├── _.backend.oto.tools.key\n ├── _.frontend.oto.tools.cer\n ├── _.frontend.oto.tools.csr\n └── _.frontend.oto.tools.key\n\n3 directories, 18 files\n```\n\n### The backend service \n\nnow, let's create a backend service using nodejs. Create a file named `backend.js`\n\n```sh\ntouch backend.js\n```\n\nand put the following content\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n}; \n\nconst server = https.createServer(options, (req, res) => { \n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nto run the server, just do \n\n```sh\nnode ./backend.js\n```\n\nnow you can try your server with\n\n```sh\ncurl --cacert ./ca/ca-backend.cer 'https://api.backend.oto.tools:8444/'\n```\n\nThis should output :\n```json\n{ \"message\": \"Hello World!\" }\n```\n\nnow modify your backend server to ensure that the client provides a client certificate like:\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n requestCert: true, \n rejectUnauthorized: true\n}; \n\nconst server = https.createServer(options, (req, res) => { \n console.log('Client certificate CN: ', req.socket.getPeerCertificate().subject.CN);\n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nyou can test your new server with\n\n```sh\ncurl \\\n --cacert ./ca/ca-backend.cer \\\n --cert ./client/_.backend.oto.tools.pem \\\n --key ./client/_.backend.oto.tools.key 'https://api.backend.oto.tools:8444/'\n```\n\nthe output should be :\n\n```json\n{ \"message\": \"Hello World!\" }\n```\n\n### Otoroshi setup\n\nDownload the latest version of the Otoroshi jar and run it like\n\n```sh\n java \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.ssl.fromOutside.clientAuth=Want \\\n -jar -Dotoroshi.storage=file otoroshi.jar\n\n[info] otoroshi-env - Admin API exposed on http://otoroshi-api.oto.tools:8080\n[info] otoroshi-env - Admin UI exposed on http://otoroshi.oto.tools:8080\n[info] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[info] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[info] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / password\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n[info] p.c.s.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:8443\n[info] otoroshi-env - Generating a self signed SSL certificate for https://*.oto.tools ...\n```\n\nand log into otoroshi with the tuple `admin@otoroshi.io / password` displayed in the logs. \n\nOnce logged in, navigate to the routes page and create a new route.\n\n* Set a name then validate the creation\n* On frontend node, add `api.frontend.oto.tools` in the list of domains\n* On backend node, replace the target with `api.backend.oto.tools` as hostname and `8444` as port. \n\nSave the route and try to call it.\n\n```sh\ncurl 'http://api.frontend.oto.tools:8080/'\n```\n\nThis should output :\n```json\n{\"Otoroshi-Error\": \"Something went wrong, you should try later. Thanks for your understanding.\"}\n```\n\nyou should get an error due to the fact that Otoroshi doesn't know about the server certificate and the client certificate expected by the server.\n\nWe must declare the client and server certificates for `https://api.backend.oto.tools` to Otoroshi. \n\nGo to the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) and create a new item. Drag and drop the content of the `./client/_.backend.oto.tools.cer` and `./client/_.backend.oto.tools.key` files, respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you prefer to use the API, you can create an Otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.backend.oto.tools.cer ./ca/ca-backend.cer ./server/_.backend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n --data-binary @- \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle \n```\n\nnow we have to expose `https://api.frontend.oto.tools:8443` using otoroshi. \n\nCreate a second item. Copy and paste the content of `./server/_.frontend.oto.tools.cer` and `./server/_.frontend.oto.tools.key` respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you don't want to bother with UI copy/paste, you can use the import bundle api endpoint to create an otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.frontend.oto.tools.cer ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n --data-binary @- \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle\n```\n\nOnce created, go back to your route. On the target of the backend node, we have to enable the custom Otoroshi TLS.\n\n* Click on the backend node\n* Click on your target\n* Click on the Show advanced settings button\n* Click on Custom TLS setup\n* Enable the section\n* In the list of certificates, select the backend certificate\n* In the list of trusted certificates, select the frontend certificate\n* Save your route\n \nTry the following command\n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\nNow we want to enforce the fact that we want client certificate for `api.frontend.oto.tools`. \n\nSearch in the list of plugins and add the `Client Certificate Only` plugin to your route.\n\nnow if you retry \n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nyou should get an error because no client certificate is passed with the request. But if you pass the `./client/_.frontend.oto.tools.csr` client cert and the key in your curl call\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n### Client certificate matching plugin\n\nOtoroshi can restrict and check all incoming client certificates on a route.\n\nSearch in the list of plugins the `Client certificate matching` plugin and add it the the flow.\n\nSave the route and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nOur client certificate is not matched by Otoroshi. We have to add the subject DN in the configuration of the `Client certificate matching` plugin to authorize it.\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [],\n \"subjectDNs\": [\n \"CN=*.frontend.oto.tools\"\n ],\n \"issuerDNs\": [],\n \"regexSubjectDNs\": [],\n \"regexIssuerDNs\": []\n }\n}\n```\n\nSave the service and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n\n"},{"name":"export-alerts-using-mailgun.md","id":"/how-to-s/export-alerts-using-mailgun.md","url":"/how-to-s/export-alerts-using-mailgun.html","title":"Send alerts using mailgun","content":"# Send alerts using mailgun\n\nAll Otoroshi alerts can be send on different channels.\nOne of the ways is to send a group of specific alerts via emails.\n\nTo enable this behaviour, let's start by create an exporter of events.\n\nIn this tutorial, we will admit that you already have a mailgun account with an API key and a domain.\n\n## Create an Mailgun exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `mailer` in the `type` selector field\n6. Jump to `Exporter config` and select the `Mailgun` option\n7. Set the following values:\n* `EU` : false/true depending on your mailgun configuratin\n* `Mailgun api key` : your-mailgun-api-key\n* `Mailgun domain` : your-mailgun-domain\n* `Email addresses` : list of the recipient adresses\n\nWith this configuration, all Otoroshi events will be send to your listed addresses (we don't recommended to do that).\n\nTo filter events on `Alerts` type, we need to add the following configuration inside the `Filtering and projection` section (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)).\n\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n``` \n\nSave at the bottom page and enable the exporter (on the top of the page or in list of exporters). We will need to wait few seconds to receive the first alerts.\n\nThe **projection** field can be useful in the case you want to filter the fields contained in each alert sent.\n\nThe `Projection` field is a json where you can list the fields to keep for each alert.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be sent to the addresses of your recipients."},{"name":"export-events-to-elastic.md","id":"/how-to-s/export-events-to-elastic.md","url":"/how-to-s/export-events-to-elastic.html","title":"Export events to Elasticsearch","content":"# Export events to Elasticsearch\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Deploy a Elasticsearch and kibana stack on Docker\n\nLet's start by creating an Elasticsearch and Kibana stack on our machine (if it's already done for you, you can skip this section).\n\nTo start an Elasticsearch container for development or testing, run:\n\n```sh\ndocker network create elastic\ndocker pull docker.elastic.co/elasticsearch/elasticsearch:7.15.1\ndocker run --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e \"discovery.type=single-node\" docker.elastic.co/elasticsearch/elasticsearch:7.15.1\n```\n\n```sh\ndocker pull docker.elastic.co/kibana/kibana:7.15.1\ndocker run --name kib01-test --net elastic -p 5601:5601 -e \"ELASTICSEARCH_HOSTS=http://es01-test:9200\" docker.elastic.co/kibana/kibana:7.15.1\n```\n\nTo access Kibana, go to @link:[http://localhost:5601](http://localhost:5601) { open=new }.\n\n### Create an Elasticsearch exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `elastic` in the `type` selector field\n6. Jump to `Exporter config`\n7. Set the following values: `Cluster URI` -> `http://localhost:9200`\n\nThen test your configuration by clicking on the `Check connection` button. This should output a modal with the Elasticsearch version and the number of loaded docs.\n\nSave at the bottom of the page and enable the exporter (on the top of the page or in list of exporters).\n\n### Testing your configuration\n\nOne simple way to test is to setup the reading of our Elasticsearch instance by Otoroshi.\n\nNavigate to the danger zone (click on the cog on the top right and scroll to `danger zone`). Jump to the `Analytics: Elastic dashboard datasource (read)` section.\n\nSet the following values : `Cluster URI` -> `http://localhost:9200`\n\nThen click on the `Check connection`. This should ouput the same result as the previous part. Save the global configuration and navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/stats](http://otoroshi.oto.tools:8080/bo/dashboard/stats) { open=new }.\n\nThis should output a list of graphs.\n\n### Advanced usage\n\nBy default, an exporter handle all events from Otoroshi. In some case, you need to filter the events to send to elasticsearch.\n\nTo filter the events, jump to the `Filtering and projection` field in exporter view. Otoroshi supports to include a kind of events or to exclude a list of events (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)). \n\nAn example which keep only events with a field `@type` of value `AlertEvent`:\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n```\nAn example which exclude only events with a field `@type` of value `GatewayEvent` :\n```json\n{\n \"exclude\": [\n { \"@type\": \"GatewayEvent\" }\n ],\n \"include\": []\n}\n```\n\nThe next field is the **Projection**. This field is a json when you can list the fields to keep for each event.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be send to ES.\n\n### Debug your configuration\n\n#### Missing user rights on Elasticsearch\n\nWhen creating an exporter, Otoroshi try to join the index route of the elasticsearch instance. If you have a specific management access rights on Elasticsearch, you have two possiblities :\n\n- set a full access to the user used in Otoroshi for write in Elasticsearch\n- set the version of Elasticsearch inside the `Version` field of your exporter.\n\n#### None event appear in your Elasticsearch\n\nWhen creating an exporter, Otoroshi try to push the index template on Elasticsearch. If the post failed, Otoroshi will fail for each push of events and your database will keep empty. \n\nTo fix this problem, you can try to send the index template with the `Manually apply index template` button in your exporter."},{"name":"import-export-otoroshi-datastore.md","id":"/how-to-s/import-export-otoroshi-datastore.md","url":"/how-to-s/import-export-otoroshi-datastore.html","title":"Import and export Otoroshi datastore","content":"# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`"},{"name":"index.md","id":"/how-to-s/index.md","url":"/how-to-s/index.html","title":"How to's","content":"# How to's\n\nin this section, we will explain some mainstream Otoroshi usage scenario's \n\n* @ref:[Otoroshi and WASM](./wasm-usage.md)\n* @ref:[Wasmo](./wasmo-installation.md)\n* @ref:[Tailscale integration](./tailscale-integration.md)\n* @ref:[End-to-end mTLS](./end-to-end-mtls.md)\n* @ref:[Send alerts by emails](./export-alerts-using-mailgun.md)\n* @ref:[Export events to Elasticsearch](./export-events-to-elastic.md)\n* @ref:[Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* @ref:[Secure an app with Auth0](./secure-app-with-auth0.md)\n* @ref:[Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* @ref:[Secure an app with LDAP](./secure-app-with-ldap.md)\n* @ref:[Secure an api with apikeys](./secure-with-apikey.md)\n* @ref:[Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* @ref:[Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* @ref:[Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* @ref:[TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* @ref:[Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* @ref:[Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* @ref:[TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* @ref:[The resources loader](./resources-loader.md)\n* @ref:[Log levels customization](./custom-log-levels.md)\n* @ref:[Initial state customization](./custom-initial-state.md)\n* @ref:[Communicate with Kafka](./communicate-with-kafka.md)\n* @ref:[Create your custom Authentication module](./create-custom-auth-module.md)\n* @ref:[Working with Eureka](./working-with-eureka.md)\n* @ref:[Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* @ref:[Quickly expose a website and static files](./zip-backend-plugin.md)\n\n@@@ index\n\n\n* [WASM usage](./wasm-usage.md)\n* [wasmo](./wasmo-installation.md)\n* [Tailscale integration](./tailscale-integration.md)\n* [End-to-end mTLS](./end-to-end-mtls.md)\n* [Send alerts by emails](./export-alerts-using-mailgun.md)\n* [Export events to Elasticsearch](./export-events-to-elastic.md)\n* [Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* [Secure an app with Auth0](./secure-app-with-auth0.md)\n* [Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* [Secure an app with LDAP](./secure-app-with-ldap.md)\n* [Secure an api with apikeys](./secure-with-apikey.md)\n* [Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* [Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* [Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* [TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* [Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* [Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* [TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* [The resources loader](./resources-loader.md)\n* [Log levels customization](./custom-log-levels.md)\n* [Initial state customization](./custom-initial-state.md)\n* [Communicate with Kafka](./communicate-with-kafka.md)\n* [Create your custom Authentication module](./create-custom-auth-module.md)\n* [Working with Eureka](./working-with-eureka.md)\n* [Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* [Zip Backend plugin](./zip-backend-plugin.md) \n@@@\n"},{"name":"instantiate-waf-coraza.md","id":"/how-to-s/instantiate-waf-coraza.md","url":"/how-to-s/instantiate-waf-coraza.html","title":"Instantiate a WAF with Coraza","content":"# Instantiate a WAF with Coraza\n\n
\nRoute plugins:\nCoraza WAF\nOverride Host Header\n
\n\nSometimes you may want to secure an app with a [Web Appplication Firewall (WAF)](https://en.wikipedia.org/wiki/Web_application_firewall) and apply the security rules from the [OWASP Core Rule Set](https://owasp.org/www-project-modsecurity-core-rule-set/). To allow that, we integrated [the Coraza WAF](https://coraza.io/) in Otoroshi through a plugin that uses the WASM version of Coraza.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a WAF configuration\n\nfirst, go on [the features page of otoroshi](http://otoroshi.oto.tools:8080/bo/dashboard/features) and then click on the [Coraza WAF configs. item](http://otoroshi.oto.tools:8080/bo/dashboard/extensions/coraza-waf/coraza-configs). \n\nNow create a new configuration, give it a name and a description, ensure that you enabled the `Inspect req/res body` flag and save your configuration.\n\nThe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Configure Coraza and the OWASP Core Rule Set\n\nNow you can easily configure the coraza WAF in the `json` config. section. By default it should look something like :\n\n```json\n{\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n}\n```\n\nYou can find anything about it in [the documentation of Coraza](https://coraza.io/docs/tutorials/introduction/).\n\nhere we have the basic setup to apply the OWASP core rule set in detection mode only. \nSo each time Coraza will find something weird in a request, it will only log it but let the request pass.\n We can enable blocking by setting `\"SecRuleEngine On\"`\n\nwe can also deny access to the `/admin` uri by adding the following directive\n\n```json\n\"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\"\n```\n\nYou can also provide multiple profile of rules in the `directives_map` with different names and use the `per_authority_directives` object to map hostnames to a specific profile.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs/coraza-waf-demo' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\",\n \"SecRuleEngine On\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Add the WAF plugin on your route\n\nNow you can create a new route that will use your WAF configuration. Let say we want a route on `http://wouf.oto.tools:8080` to goes to `https://www.otoroshi.io`. Now add the `Coraza WAF` plugin to your route and in the configuration select the configuration you created previously.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n -H 'Content-Type: application/json' -d '\n{\n \"id\": \"route_demo\",\n \"name\": \"WAF route\",\n \"description\": \"A new route with a WAF enabled\",\n \"frontend\": {\n \"domains\": [\n \"wouf.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"www.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.wasm.proxywasm.NgCorazaWAF\",\n \"config\": {\n \"ref\": \"coraza-waf-demo\"\n },\n \"plugin_index\": {\n \"validate_access\": 0,\n \"transform_request\": 0,\n \"transform_response\": 0\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"plugin_index\": {\n \"transform_request\": 1\n }\n }\n ]\n}'\n```\n\n### Try to use an exploit ;)\n\nlet try to trigger Coraza with a Log4Shell crafted request:\n\n```sh\ncurl 'http://wouf.oto.tools:9999' -H 'foo: ${jndi:rmi://foo/bar}' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nor access to `/admin`\n\n```sh\ncurl 'http://wouf.oto.tools:9999/admin' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nif you look at otoroshi logs you will find something like :\n\n```log\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell \n [file \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\"] [line \"10608\"] [id \"944150\"] [rev \"\"] \n [msg \"Potential Remote Command Execution: Log4j / Log4shell\"] [data \"\"] [severity \"critical\"] \n [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] [tag \"application-multi\"] \n [tag \"language-java\"] [tag \"platform-multi\"] [tag \"attack-rce\"] [tag \"OWASP_CRS\"] \n [tag \"capec/1000/152/137/6\"] [tag \"PCI/6.5.2\"] [tag \"paranoia-level/1\"] [hostname \"wwwwouf.oto.tools\"] \n [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Inbound Anomaly Score Exceeded (Total Score: 5) \n [file \"@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf\"] [line \"11029\"] [id \"949110\"] [rev \"\"] \n [msg \"Inbound Anomaly Score Exceeded (Total Score: 5)\"] \n [data \"\"] [severity \"emergency\"] [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] \n [tag \"anomaly-evaluation\"] [hostname \"wwwwouf.oto.tools\"] [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"uTYakrlgMBydVGLodbz\" context_id=3 action=\"deny\" phase=\"http_response_headers\"\n...\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. [file \"\"] [line \"12914\"] \n [id \"101\"] [rev \"\"] [msg \"\"] [data \"\"] [severity \"emergency\"] [ver \"\"] [maturity \"0\"] [accuracy \"0\"] \n [hostname \"wwwwouf.oto.tools\"] [uri \"/admin\"] [unique_id \"mqXZeMdzRaVAqIiqvHf\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"mqXZeMdzRaVAqIiqvHf\" context_id=2 action=\"deny\" phase=\"http_request_headers\"\n```\n\n### Generated events\n\neach time Coraza will generate log about vunerability detection, an event will be generated in otoroshi and exporter through the usual data exporter way. The event will look like :\n\n```json\n{\n \"@id\" : \"86b647450-3cc7-42a9-aaec-828d261a8c74\",\n \"@timestamp\" : 1684938211157,\n \"@type\" : \"CorazaTrailEvent\",\n \"@product\" : \"otoroshi\",\n \"@serviceId\" : \"--\",\n \"@service\" : \"--\",\n \"@env\" : \"prod\",\n \"level\" : \"ERROR\",\n \"msg\" : \"Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell\",\n \"fields\" : {\n \"hostname\" : \"wouf.oto.tools\",\n \"maturity\" : \"0\",\n \"line\" : \"10608\",\n \"unique_id\" : \"oNbisKlXWaCdXntaUpq\",\n \"tag\" : \"paranoia-level/1\",\n \"data\" : \"\",\n \"accuracy\" : \"0\",\n \"uri\" : \"/\",\n \"rev\" : \"\",\n \"id\" : \"944150\",\n \"client\" : \"127.0.0.1\",\n \"ver\" : \"OWASP_CRS/4.0.0-rc1\",\n \"file\" : \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\",\n \"msg\" : \"Potential Remote Command Execution: Log4j / Log4shell\",\n \"severity\" : \"critical\"\n },\n \"raw\" : \"[client \\\"127.0.0.1\\\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell [file \\\"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\\\"] [line \\\"10608\\\"] [id \\\"944150\\\"] [rev \\\"\\\"] [msg \\\"Potential Remote Command Execution: Log4j / Log4shell\\\"] [data \\\"\\\"] [severity \\\"critical\\\"] [ver \\\"OWASP_CRS/4.0.0-rc1\\\"] [maturity \\\"0\\\"] [accuracy \\\"0\\\"] [tag \\\"application-multi\\\"] [tag \\\"language-java\\\"] [tag \\\"platform-multi\\\"] [tag \\\"attack-rce\\\"] [tag \\\"OWASP_CRS\\\"] [tag \\\"capec/1000/152/137/6\\\"] [tag \\\"PCI/6.5.2\\\"] [tag \\\"paranoia-level/1\\\"] [hostname \\\"wouf.oto.tools\\\"] [uri \\\"/\\\"] [unique_id \\\"oNbisKlXWaCdXntaUpq\\\"]\\n\",\n}\n```"},{"name":"resources-loader.md","id":"/how-to-s/resources-loader.md","url":"/how-to-s/resources-loader.html","title":"The resources loader","content":"# The resources loader\n\nThe resources loader is a tool to create an Otoroshi resource from a raw content. This content can be found on each Otoroshi resources pages (services descriptors, apikeys, certificates, etc ...). To get the content of a resource as file, you can use the two export buttons, one to export as JSON format and the other as YAML format.\n\nOnce exported, the content of the resource can be import with the resource loader. You can import single or multiples resources on one time, as JSON and YAML format.\n\nThe resource loader is available on this route [`bo/dashboard/resources-loader`](http://otoroshi.oto.tools:8080/bo/dashboard/resources-loader).\n\nOn this page, you can paste the content of your resources and click on **Load resources**.\n\nFor each detected resource, the loader will display :\n\n* a resource name corresponding to the field `name` \n* a resource type corresponding to the type of created resource (ServiceDescriptor, ApiKey, Certificate, etc)\n* a toggle to choose if you want to include the element for the creation step\n* the updated status by the creation process\n\nOnce you have selected the resources to create, you can **Import selected resources**.\n\nOnce generated, all status will be updated. If all is working, the status will be equals to done.\n\nIf you want to get back to the initial page, you can use the **restart** button."},{"name":"secure-an-app-with-jwt-verifiers.md","id":"/how-to-s/secure-an-app-with-jwt-verifiers.md","url":"/how-to-s/secure-an-app-with-jwt-verifiers.html","title":"Secure an api with jwt verifiers","content":"# Secure an api with jwt verifiers\n\n
\nRoute plugins:\nJwt verification only\n
\n\nA Jwt verifier is the guard that verifies the signature of tokens in requests. \n\nA verifier can obvisouly verify or generate.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Your first jwt verifier\n\nLet's start by validating all incoming request tokens tokens on our simple route created in the @ref:[Before you start](#before-you-start) section.\n\n1. Navigate to the simple route\n2. Search in the list of plugins and add the `Jwt verification only` plugin on the flow\n3. Click on `Start by select or create a JWT Verifier`\n4. Create a new JWT verifier\n5. Set `simple-jwt-verifier` as `Name`\n6. Select `Hmac + SHA` as `Algo` (for this example, we expect tokens with a symetric signature), `512` as `SHA size` and `otoroshi` as `HMAC secret`\n7. Confirm the creation \n\nSave your route and try to call it\n\n```sh\ncurl -X GET 'http://myservice.oto.tools:8080/' --include\n```\n\nThis should output : \n```json\n{\n \"Otoroshi-Error\": \"error.expected.token.not.found\"\n}\n```\n\nA simple way to generate a token is to use @link:[jwt.io](http://jwt.io) { open=new }. Once navigate, define `HS512` as `alg` in header section and insert `otoroshi` as verify signature secret. \n\nOnce created, copy-paste the token from jwt.io to the Authorization header and call our service.\n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"X-JWT-Token: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `X-JWT-Token` in headers field. Its value is exactly the same as the passed token.\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"X-JWT-Token\": \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ipDFgkww51mSaSg_199BMRj4gK20LGz_czozu3u8rCFFO1X20MwcabSqEzUc0q4qQ4rjTxjoR4HeUDVcw8BxoQ\",\n ...\n }\n}\n```\n\n### Verify and generate a new token\n\nAn other feature is to verify the incomings tokens and generate new ones, with a different signature and claims. \n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify and re-sign JWT token`. \n2. Edit the name with `jwt-verify-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Save your verifier.\n\n> Note : the name of the verifier doesn't impact the identifier. So you can save the changes of your verifier without modifying the identifier used in your call. \n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"Authorization: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `authorization` in headers field. This time, the value are different and you can check his signature on @link:[jwt.io](https://jwt.io) { open=new } (the expected secret of the generated token is **otoroshi-internal-secret**)\n\n\n\n### Verify, transform and generate a new token\n\nThe most advanced verifier is able to do the same as the previous ones, with the ability to configure the token generation (claims, output header name).\n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify, transform and re-sign JWT token`. \n\n2. Edit the name with `jwt-verify-transform-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Set `Internal-Authorization` as `Header name`\n6. Set `key` on first field of `Rename token fields` and `from-otoroshi-verifier` on second field\n7. Set `generated-key` and `generated-value` as `Set token fields`\n8. Add `generated_at` and `${date}` as second field of `Set token fields` (Otoroshi supports an @ref:[expression language](../topics/expression-language.md))\n9. Save your verifier and try to call your service again.\n\nThis should output a json with `authorization` in headers field and our generate token in `Internal-Authorization`.\nOnce paste in @link:[jwt.io](https://jwt.io) { open=new }, you should have :\n\n\n\nYou can see, in the payload of your token, the two claims **from-otoroshi-verifier** and **generated-key** added during the generation of the token by the JWT verifier.\n"},{"name":"secure-app-with-auth0.md","id":"/how-to-s/secure-app-with-auth0.md","url":"/how-to-s/secure-app-with-auth0.html","title":"Secure an app with Auth0","content":"# Secure an app with Auth0\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Configure an Auth0 client\n\nThe first step of this tutorial is to setup an Auth0 application with the information of the instance of our Otoroshi.\n\nNavigate to @link:[https://manage.auth0.com](https://manage.auth0.com) { open=new } (create an account if it's not already done). \n\nLet's create an application when clicking on the **Applications** button on the sidebar. Then click on the **Create application** button on the top right.\n\n1. Choose `Regular Web Applications` as `Application type`\n2. Then set for example `otoroshi-client` as `Name`, and confirm the creation\n3. Jump to the `Settings` tab\n4. Scroll to the `Application URLs` section and add the following url as `Allowed Callback URLs` : `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n5. Set `https://otoroshi.oto.tools:8080/` as `Allowed Logout URLs`\n6. Set `https://otoroshi.oto.tools:8080` as `Allowed Web Origins` \n7. Save changes at the bottom of the page.\n\nOnce done, we have a full setup, with a client ID and secret at the top of the page, which authorizes our Otoroshi and redirects the user to the callback url when they log into Auth0.\n\n### Create an Auth0 provider module\n\nLet's back to Otoroshi to create an authentication module with `OAuth2 / OIDC provider` as `type`.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth provider` in the type selector field\n3. Then click on `Get from OIDC config` and paste `https://..auth0.com/.well-known/openid-configuration`. Replace the tenant name by the name of your tenant (displayed on the left top of auth0 page), and the region of the tenant (`eu` in my case).\n\nOnce done, set the `Client ID` and the `Client secret` from your Auth0 application. End the configuration with `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`.\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs).\n\n### Connect to Otoroshi with Auth0 authentication\n\nTo secure Otoroshi with your Auth0 configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n2. Scroll to the **BackOffice auth. settings**\n3. Select your last Authentication configuration (created in the previous section)\n4. Save the global configuration with the button on the top right\n\n#### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the *Login using third-party* button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the auth0 server login page\n4. Set your account credentials\n5. Good works! You're connected to Otoroshi with an Auth0 module.\n\n### Secure an app with Auth0 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new **Authentication module** configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a connected user by a third-party server is covered by another route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called **a private app** when it is protected by an Authentication module.\n\nWe can set the Authentication module on your route.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n5. Now you can try to call your route and see the Auth0 login page appears.\n\n\n"},{"name":"secure-app-with-keycloak.md","id":"/how-to-s/secure-app-with-keycloak.md","url":"/how-to-s/secure-app-with-keycloak.html","title":"Secure an app with Keycloak","content":"# Secure an app with Keycloak\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running a keycloak instance with docker\n\n```sh\ndocker run \\\n -p 8080:8080 \\\n -e KEYCLOAK_USER=admin \\\n -e KEYCLOAK_PASSWORD=admin \\\n --name keycloak-server \\\n --detach jboss/keycloak:15.0.1\n```\n\nThis should download the image of keycloak (if you haven't already it) and display the digest of the created container. This command mapped TCP port 8080 in the container to port 8080 of your laptop and created a server with `admin/admin` as admin credentials.\n\nOnce started, you can open a browser on @link:[http://localhost:8080](http://localhost:8080) { open=new } and click on `Administration Console`. Log to your instance with `admin/admin` as credentials.\n\nThe first step is to create a Keycloak client, an entity that can request Keycloak to authenticate a user. Click on the **clients** button on the sidebar, and then on **Create** button at the top right of the view.\n\nFill the client form with the following values.\n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Protocol`: `openid-connect`\n* `Root URL`: `http://otoroshi.oto.tools:8080/`\n\nValidate the creation of the client by clicking on the **Save** button.\n\nThe next step is to change the `Access Type` used by default. Jump to the `Access Type` field and select `confidential`. The confidential configuration force the client application to send at Keycloak a client ID and a client Secret. Scroll to the bottom of the page and save the configuration.\n\nNow scroll to the top of your page. Just at the right of the `Settings` tab, a new tab appeared : the `Credentials` page. Click on this tab, and make sure that `Client Id and Secret` is selected as `Client Authenticator` and copy the generated `Secret` to the next part.\n\n### Create a Keycloak provider module\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth2 / OIDC provider` in the type selector field\n3. Set a basic name and description\n\nA simple way to import a Keycloak client is to give the `URL of the OpenID Connect` Otoroshi. By default, keycloak used the next URL : `http://localhost:8080/auth/realms/master/.well-known/openid-configuration`. \n\nClick on the `Get from OIDC config` button and paste the previous link. Once it's done, scroll to the `URLs` section. All URLs has been fill with the values picked from the JSON object returns by the previous URL.\n\nThe only fields to change are : \n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Secret`: Paste the secret from the Credentials Keycloak page. In my case, it's something like `90c9bf0b-2c0c-4eb0-aa02-72195beb9da7`\n* `Callback URL`: `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs). Nothing else to change, just save the configuration.\n\n### Connect to Otoroshi with Keycloak authentication\n\nTo secure Otoroshi with your Keycloak configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new })\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the keycloak login page\n4. Set `admin/admin` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an Keycloak module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Visualize an admin user session or a private user session\n\nEach user, wheter connected user to the Otoroshi UI or at a private Otoroshi app, has an own session. As an administrator of Otoroshi, you can visualize via Otoroshi the list of the connected users and their profile.\n\nLet's start by navigating to the `Admin users sessions` page (just @link:[here](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/admin) or when clicking on the cog, and on the `Admins sessions` button at the bottom of the list).\n\nThis page gives a complete view of the connected admins. For each admin, you have his connection date and his expiration date. You can also check the `Profile` and the `Rights` of the connected users.\n\nIf we check the profile and the rights of the previously logged user (from Keycloak in the previous part) we can retrieve the following information :\n\n```json\n{\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"upn\": \"admin\",\n \"email_verified\": false,\n \"address\": {},\n \"groups\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ],\n \"preferred_username\": \"admin\"\n}\n```\n\nand his default rights \n\n```sh\n[\n {\n \"tenant\": \"default:rw\",\n \"teams\": [\n \"default:rw\"\n ]\n }\n]\n```\n\nWe haven't create any specific groups in Keycloak or specify rights in Otoroshi for him. In this case, the use received the default Otoroshi rights at his connection. The user can navigate on the default Organization and Teams (which are two resources created by Otoroshi at the boot) and have the full access on its (`r`: Read, `w`: Write, `*`: read/write).\n\nIn the same way, you'll find all users connected to a private Otoroshi app when navigate on the @link:[`Private App View`](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) or using the cog at the top of the page. \n\n### Configure the Keycloak module to force logged in users to be an Otoroshi admin with full access\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Supers admin only` button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, click on the cog button, and check that you have access to the full features of Otoroshi (like Admin user sessions). Now, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n]\n```\n\n### Merge Id token content on user profile\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Read profile` from token button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, your profile should be contains all Keycloak id token : \n```json\n{\n \"exp\": 1634286674,\n \"iat\": 1634286614,\n \"auth_time\": 1634286614,\n \"jti\": \"eb368578-e886-4caa-a51b-c1d04973c80e\",\n \"iss\": \"http://localhost:8080/auth/realms/master\",\n \"aud\": [\n \"master-realm\",\n \"account\"\n ],\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"typ\": \"Bearer\",\n \"azp\": \"keycloak-otoroshi-backoffice\",\n \"session_state\": \"e44fe471-aa3b-477d-b792-4f7b4caea220\",\n \"acr\": \"1\",\n \"allowed-origins\": [\n \"http://otoroshi.oto.tools:8080\"\n ],\n \"realm_access\": {\n \"roles\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ]\n },\n \"resource_access\": {\n \"master-realm\": {\n \"roles\": [\n \"view-identity-providers\",\n \"view-realm\",\n \"manage-identity-providers\",\n \"impersonation\",\n \"create-client\",\n \"manage-users\",\n \"query-realms\",\n \"view-authorization\",\n \"query-clients\",\n \"query-users\",\n \"manage-events\",\n \"manage-realm\",\n \"view-events\",\n \"view-users\",\n \"view-clients\",\n \"manage-authorization\",\n \"manage-clients\",\n \"query-groups\"\n ]\n },\n \"account\": {\n \"roles\": [\n \"manage-account\",\n \"manage-account-links\",\n \"view-profile\"\n ]\n }\n }\n ...\n}\n```\n\n### Manage the Otoroshi user rights from keycloak\n\nOne powerful feature supports by Otoroshi, is to use the Keycloak groups attributes to set a list of rights for a Otoroshi user.\n\nIn the Keycloak module, you have a field, named `Otoroshi rights field name` with `otoroshi_rights` as default value. This field is used by Otoroshi to retrieve information from the Id token groups.\n\nLet's create a group in Keycloak, and set our default Admin user inside.\nIn Keycloak admin console :\n\n1. Navigate to the groups view, using the keycloak sidebar\n2. Create a new group with `my-group` as `Name`\n3. Then, on the `Attributes` tab, create an attribute with `otoroshi_rights` as `Key` and the following json array as `Value`\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\nWith this configuration, the user have a full access on all Otoroshi resources (my-future-team is not created in Otoroshi but it's not a problem, Otoroshi can handle it and use this rights only when the team will be present)\n\nClick on the **Add** button and **save** the group. The last step is to assign our user to this group. Jump to `Users` view using the sidebar, click on **View all users**, edit the user and his group membership using the `Groups` tab (use **join** button the assign user in `my-group`).\n\nThe next step is to add a mapper in the Keycloak client. By default, Keycloak doesn't expose any users information (like group membership or users attribute). We need to ask to Keycloak to expose the user attribute `otoroshi_rights` set previously on group.\n\nNavigate to the `Keycloak-otoroshi-backoffice` client, and jump to `Mappers` tab. Create a new mapper with the following values: \n\n* Name: `otoroshi_rights`\n* Mapper Type: `User Attribute`\n* User Attribute: `otoroshi_rights`\n* Token Claim Name: `otoroshi_rights`\n* Claim JSON Type: `JSON`\n* Multivalued: `√`\n* Aggregate attribute values: `√`\n\nGo back to the Authentication Keycloak module inside Otoroshi UI, and turn off **Super admins only**. **Save** the configuration.\n\nOnce done, try again the connection to Otoroshi using Keycloak third-party server.\nNow, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\n### Secure an app with Keycloak authentication\n\nThe only change to apply on the previous authentication module is on the callback URL. When you want secure a Otoroshi service, and transform it on `Private App`, you need to set the `Callback URL` at `http://privateapps.oto.tools:8080/privateapps/generic/callback`. This configuration will redirect users to the backend service after they have successfully logged in.\n\n1. Go back to the authentication module\n2. Jump to the `Callback URL` field\n3. Paste this value `http://privateapps.oto.tools:8080/privateapps/generic/callback`\n4. Save your configuration\n5. Navigate to `http://myservice.oto.tools:8080`.\n6. You should redirect to the keycloak login page.\n7. Once logged in, you can check the content of the private app session created.\n\nThe rights should be : \n\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```"},{"name":"secure-app-with-ldap.md","id":"/how-to-s/secure-app-with-ldap.md","url":"/how-to-s/secure-app-with-ldap.html","title":"Secure an app and/or your Otoroshi UI with LDAP","content":"# Secure an app and/or your Otoroshi UI with LDAP\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n#### Running an simple OpenLDAP server \n\nRun OpenLDAP docker image : \n```sh\ndocker run \\\n -p 389:389 \\\n -p 636:636 \\\n --env LDAP_ORGANISATION=\"Otoroshi company\" \\\n --env LDAP_DOMAIN=\"otoroshi.tools\" \\\n --env LDAP_ADMIN_PASSWORD=\"otoroshi\" \\\n --env LDAP_READONLY_USER=\"false\" \\\n --env LDAP_TLS\"false\" \\\n --env LDAP_TLS_ENFORCE\"false\" \\\n --name my-openldap-container \\\n --detach osixia/openldap:1.5.0\n```\n\nLet's make the first search in our LDAP container :\n\n```sh\ndocker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=otoroshi,dc=tools -D \"cn=admin,dc=otoroshi,dc=tools\" -w otoroshi\n```\n\nThis should output :\n```sh\n# extended LDIF\n ...\n# otoroshi.tools\ndn: dc=otoroshi,dc=tools\nobjectClass: top\nobjectClass: dcObject\nobjectClass: organization\no: Otoroshi company\ndc: otoroshi\n\n# search result\nsearch: 2\nresult: 0 Success\n...\n```\n\nNow you can seed the open LDAP server with a few users. \n\nJoin your LDAP container.\n\n```sh\ndocker exec -it my-openldap-container \"/bin/bash\"\n```\n\nThe command `ldapadd` needs of a file to run.\n\nLaunch this command to create a `bootstrap.ldif` with one organization, one singers group with John user and a last group with Baz as scientist.\n\n```sh\necho -e \"\ndn: ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: People\n\ndn: ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: Role\n\ndn: uid=john,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: john\ncn: John\nsn: Brown\nmail: john@otoroshi.tools\npostalCode: 88442\nuserPassword: password\n\ndn: uid=baz,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: baz\ncn: Baz\nsn: Wilson\nmail: baz@otoroshi.tools\npostalCode: 88443\nuserPassword: password\n\ndn: cn=singers,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: singers\nmember: uid=john,ou=People,dc=otoroshi,dc=tools\n\ndn: cn=scientists,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: scientists\nmember: uid=baz,ou=People,dc=otoroshi,dc=tools\n\" > bootstrap.ldif\n\nldapadd -x -w otoroshi -D \"cn=admin,dc=otoroshi,dc=tools\" -f bootstrap.ldif -v\n```\n\n### Create an Authentication configuration\n\n- Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n- Click on the cog icon on the top right\n- Then `Authentication configs` button\n- And add a new configuration when clicking on the `Add item` button\n- Select the `Ldap auth. provider` in the type selector field\n- Set a basic name and description\n- Then set `ldap://localhost:389` as `LDAP Server URL`and `dc=otoroshi,dc=tools` as `Search Base`\n- Create a group filter (in the next part, we'll change this filter to spread users in different groups with given rights) with \n - objectClass=groupOfNames as `Group filter` \n - All as `Tenant`\n - All as `Team`\n - Read/Write as `Rights`\n- Set the search filter as `(uid=${username})`\n- Set `cn=admin,dc=otoroshi,dc=tools` as `Admin username`\n- Set `otoroshi` as `Admin password`\n- At the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n\n At this point, your configuration should be similar to :\n \n\n\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n- Test the connection when clicking on `Test admin connection` button. This should show a `It works!` message\n\n- Finally, test the user connection button and set `john/password` or `baz/password` as credentials. This should show a `It works!` message\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n\n### Connect to Otoroshi with LDAP authentication\n\nTo secure Otoroshi with your LDAP configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n- Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n- Scroll to the **BackOffice auth. settings**\n- Select your last Authentication configuration (created in the previous section)\n- Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n- Disconnect from your instance\n- Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080/backoffice/auth0/login](http://otoroshi.oto.tools:8080/backoffice/auth0/login) { open=new })\n- Set `john/password` or `baz/password` as credentials\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n\n#### Secure an app with LDAP authentication\n\nOnce the configuration is done, you can secure any of Otoroshi routes. \n\n- Navigate to any created route\n- Add the `Authentication` plugin to your route\n- Select your Authentication config inside the list\n- Save your configuration\n\nNow try to call your route. The login module should appear.\n\n#### Manage LDAP users rights on Otoroshi\n\nFor each group filter, you can affect a list of rights:\n\n- on an `Organization`\n- on a `Team`\n- and a level of rights : `Read`, `Write` or `Read/Write`\n\n\nStart by navigate to your authentication configuration (created in @ref:[previous](#create-an-authentication-configuration) step).\n\nThen, replace the values of the `Mapping group filter` field to match LDAP groups with Otoroshi rights.\n\n\n\n\nWith this configuration, Baz is an administrator of Otoroshi with full rights (read / write) on all organizations.\n\nConversely, John can't see any configuration pages (like the danger zone) because he has only the read rights on Otoroshi.\n\nYou can easily test this behaviour by @ref:[testing](#testing-your-configuration) with both credentials.\n\n\n#### Advanced usage of LDAP Authentication\n\nIn the previous section, we have define rights for each LDAP groups. But in some case, we want to have a finer granularity like set rights for a specific user. The last 4 fields of the authentication form cover this. \n\nLet's start by adding few properties for each connected users with `Extra metadata`.\n\n```json\n// Add this configuration in extra metadata part\n{\n \"provider\": \"OpenLDAP\"\n}\n```\n\nThe next field `Data override` is merged with extra metadata when a user connects to a `private app` or to the UI (inside Otoroshi, private app is a service secure by any authentication module). The `Email field name` is configured to match with the `mail` field from LDAP user data.\n\n```json \n{\n \"john@otoroshi.tools\": {\n \"stage_name\": \"Will\"\n }\n}\n```\n\nIf you try to connect to an app with this configuration, the user result profile should be :\n\n```json\n{\n ...,\n \"metadata\": {\n \"lastname\": \"Willy\",\n \"stage_name\": \"Will\"\n }\n}\n```\n\nLet's try to increase the John rights with the `Additional rights group`.\n\nThis field supports the creation of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n\n```json\n// increase_john_rights is a virtual group which adds full access rights at john \n{\n \"increase_john_rights\": {\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ],\n \"users\": [\n \"john@otoroshi.tools\"\n ]\n }\n}\n```\n\nThe last field `Rights override` is useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights. \n\nTo resume, when John connects to Otoroshi, he receives the rights to only read the default Organization (from **Mapping group filter**), then he is promote to administrator role (from **Additional rights group**) and finally his rights are reset with the last field **Rights override** to the read rights.\n\n```json \n{\n \"john@otoroshi.tools\": [\n {\n \"tenant\": \"*:r\",\n \"teams\": [\n \"*:r\"\n ]\n }\n ]\n}\n```\n\n\n\n\n\n\n\n\n"},{"name":"secure-the-communication-between-a-backend-app-and-otoroshi.md","id":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.md","url":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html","title":"Secure the communication between a backend app and Otoroshi","content":"# Secure the communication between a backend app and Otoroshi\n\n
\nRoute plugins:\nOtoroshi challenge token\n
\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nLet's create a new route with the Otorochi challenge plugin enabled.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 8081,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiChallenge\",\n \"config\": {\n \"version\": 2,\n \"ttl\": 30,\n \"request_header_name\": \"Otoroshi-State\",\n \"response_header_name\": \"Otoroshi-State-Resp\",\n \"algo_to_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"algo_from_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"state_resp_leeway\": 10\n }\n }\n ]\n}\nEOF\n```\n\nLet's use the following application, developed in NodeJS, which supports both versions of the exchange protocol.\n\nClone this @link:[repository](https://github.com/MAIF/otoroshi/blob/master/demos/challenge) and run the installation of the dependencies.\n\n```sh\ngit clone 'git@github.com:MAIF/otoroshi.git' --depth=1\ncd ./otoroshi/demos/challenge\nnpm install\nPORT=8081 node server.js\n```\n\nThe last command should return : \n\n```sh\nchallenge-verifier listening on http://0.0.0.0:8081\n```\n\nThis project runs an express client with one middleware. The middleware handles each request, and check if the header `State token header` is present in headers. By default, the incoming expected header is `Otoroshi-State` by the application and `Otoroshi-State-Resp` header in the headers of the return request. \n\nTry to call your service via http://myapi.oto.tools:8080/. This should return a successful response with all headers received by the backend app. \n\nNow try to disable the middleware in the nodejs file by commenting the following line. \n\n```js\n// app.use(OtoroshiMiddleware());\n```\n\nTry to call again your service. This time, Otoroshi breaks the return response from your backend service, and returns.\n\n```sh\nDownstream microservice does not seems to be secured. Cancelling request !\n```"},{"name":"secure-with-apikey.md","id":"/how-to-s/secure-with-apikey.md","url":"/how-to-s/secure-with-apikey.html","title":"Secure an api with api keys","content":"# Secure an api with api keys\n\n
\nRoute plugins:\nApikeys\n
\n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n### Create a simple route\n\n**From UI**\n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/routes](http://otoroshi.oto.tools:8080/bo/dashboard/routes) { open=new } and click on the `create new route` button\n2. Give a name to your route\n3. Save your route\n4. Set `myservice.oto.tools` as frontend domains\n5. Set `https://mirror.otoroshi.io` as backend target (hostname: `mirror.otoroshi.io`, port: `443`, Tls: `Enabled`)\n\n**From Admin API**\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n### Secure routes with api key\n\nBy default, a route is public. In our case, we want to secure all paths starting with `/api` and leave all others unauthenticated.\n\nLet's add a new plugin, called `Apikeys`, to our route. Search in the list of plugins, then add it to the flow.\nOnce done, restrict its range by setting up `/api` in the `Informations>include` section.\n\n**From Admin API**\n\n```sh\ncurl -X PUT http://otoroshi-api.oto.tools:8080/api/routes/myservice \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/api\"\n ],\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to @link:[http://myservice.oto.tools:8080/api/test](http://myservice.oto.tools:8080/api/test) { open=new } again. If the service is configured, you should have a `Service Not found error`.\n\nThe expected error on the `/api/test`, indicate that an api key is required to access to this part of the backend service.\n\nNavigate to any other routes which are not starting by `/api/*` like @link:[http://myservice.oto.tools:8080/test/bar](http://myservice.oto.tools:8080/test/bar) { open=new }\n\n\n### Generate an api key to request secure services\n\nNavigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add](http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add) { open=new } or when clicking on the **Add apikey** button on the sidebar.\n\nThe only required fields of an Otoroshi api key are : \n\n* `ApiKey id`\n* `ApiKey Secret`\n* `ApiKey Name`\n\nThese fields are automatically generated by Otoroshi. However, you can override these values and indicate an additional description.\n\nTo simplify the rest of the tutorial, set the values:\n\n* `my-first-api-key-id` as `ApiKey Id`\n* `my-first-api-key-secret` as `ApiKey Secret`\n\nClick on **Create and stay on this ApiKey** button at the bottom of the page.\n\nNow you created the key, it's time to call our previous generated service with it.\n\nOtoroshi supports 4 methods to achieve that: \n\nFirst one by passing Otoroshi api key in two headers : `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` (these headers names can be override on each service).\nThe second by passing Otoroshi api key in the authentication Header (basically the `Authorization` header) as a basic encoded value. The third option is to use the bearer generated for your apikey (you can get it by calling `curl http://otoroshi-api.oto.tools:8080/api/apikeys/my-first-api-key-id/bearer`). A fourth option is to use jwt token but we will not review it here but you can find a @ref[tutorial here](./secure-with-oauth2-client-credentials.md).\n\nLet's go ahead and call our service with the first method :\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the second method using basic authentication:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nor\n\n```sh\ncurl -X GET \\\n -u my-first-api-key-id:my-first-api-key-secret \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the third method using otoroshi bearer:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Bearer otoapk_my-first-api-key-id_99cb8e081d692044593ad0e658a67a5114f7afbdcbeb26f8087cce0df3b610b2' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\n> Tips : To easily fill your headers, you can jump to the `Call examples` section in each api key view. In this section the header names are the default values and the service url is not set. You have to adapt these lines to your case. \n\n### Override defaults headers names for a route\n\nIn some case, we want to change the defaults headers names (and it's a quite good idea).\n\nLet's start by navigating to the `Apikeys` plugin in the Designer of our route.\n\nThe first values to change are the headers names used to read the api key from client. Start by clicking on `extractors > CustomHeaders` and set the following values :\n\n* `api-key-header-id` as `Custom client id header name`\n* `api-key-header-secret` as `Custom client secret header name`\n\nSave the route, and call the service again.\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output an error because Otoroshi are expecting the api keys in other headers.\n\n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nCall one again the service but with the changed headers names.\n\n```sh\ncurl -X GET \\\n -H 'api-key-header-id: my-first-api-key-id' \\\n -H 'api-key-header-secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nAll others default services will continue to accept the api keys with the `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` headers, whereas our service, will accept the `api-key-header-id` and `api-key-header-secret` headers.\n\n### Accept only api keys with expected values\n\nBy default, a secure service only accepts requests with api key. But all generated api keys are eligible to call our service and in some case, we want authorize only a couple of api keys.\n\nYou can restrict the list of accepted api keys by giving a list of `metadata` or/and `tags`. Each api key has a list of `tags` and `metadata`, which can be used by Otoroshi to validate a request with an api key. All api key metadata/tags can be forward to your service (see `Otoroshi Challenge` section of a service to get more information about `Otoroshi info. token`).\n\nLet's starting by only accepting api keys with the `otoroshi` tag.\n\nClick on the `ApiKeys` plugin, and enabled the `Routing` section. These constraints guarantee that a request will only be transmitted if all the constraints are validated.\n\nIn our first case, set `otoroshi` in `One Tag in` array and save the service.\nThen call our service with :\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output :\n```json\n// Error reason : Our api key doesn't contains the expected tag.\n{\n \"Otoroshi-Error\": \"Bad API key\"\n}\n```\n\nNavigate to the edit page of our api key, and jump to the `Metadata and tags` section.\nIn this section, add `otoroshi` in `Tags` array, then save the api key. Call once again your call and you will normally get a successful response of our backend service.\n\nIn this example, we have limited our service to API keys that have `otoroshi` as a tag.\n\nOtoroshi provides a few others behaviours. For each behaviour, *Api key used should*:\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n\n---\n\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n \n----\n\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata"},{"name":"secure-with-oauth1-client.md","id":"/how-to-s/secure-with-oauth1-client.md","url":"/how-to-s/secure-with-oauth1-client.html","title":"Secure an app with OAuth1 client flow","content":"# Secure an app with OAuth1 client flow\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running an simple OAuth 1 server\n\nIn this tutorial, we'll instantiate a oauth 1 server with docker. If you alredy have the necessary, skip this section @ref:[to](#create-an-oauth-1-provider-module).\n\nLet's start by running the server\n\n```sh\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://otoroshi.oto.tools:8080/backoffice/auth0/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nWe created a oauth 1 server which accepts `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Redirect URI`. This URL is used by Otoroshi to retrieve a token and a profile at the end of an authentication process.\n\nAfter this command, the container logs should output :\n```sh \n127.0.0.1 - - [14/Oct/2021 12:10:49] \"HEAD /api/health HTTP/1.1\" 200 -\n```\n\n### Create an OAuth 1 provider module\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then **Authentication configs** button\n1. And add a new configuration when clicking on the **Add item** button\n2. Select the `Oauth1 provider` in the type selector field\n3. Set a basic name and description like `oauth1-provider`\n4. Set `2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET` as `Consumer key`\n5. Set `wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp` as `Consumer secret`\n6. Set `http://localhost:5000/oauth/request_token` as `Request Token URL`\n7. Set `http://localhost:5000/oauth/authorize` as `Authorize URL`\n8. Set `http://localhost:oauth/access_token` as `Access token URL`\n9. Set `http://localhost:5000/api/me` as `Profile URL`\n10. Set `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`\n11. At the bottom of the page, disable the **secure** button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n At this point, your configuration should be similar to :\n\n\n\n\nWith this configuration, the connected user will receive default access on teams and organizations. If you want to change the access rights for a specific user, you can achieve it with the `Rights override` field and a configuration like :\n\n```json\n{\n \"foo@example.com\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n}\n```\n\nSave your configuration at the bottom of the page, then navigate to the `danger zone` to use your module as a third-party connection to the Otoroshi UI.\n\n### Connect to Otoroshi with OAuth1 authentication\n\nTo secure Otoroshi with your OAuth1 configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the oauth 1 server login page\n4. Set `example-user` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an OAuth1 module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Secure an app with OAuth 1 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new auth module configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a logged user by a third-party server is cover by an other route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called a private app when it is protected by an authentication module.\n\nOur example server supports only one redirect URI. We need to kill it, and to create a new container with `http://otoroshi.oto.tools:8080/privateapps/generic/callback` as `OAUTH1_REDIRECT_URI`\n\n```sh\ndocker rm -f oauth1-server\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://privateapps.oto.tools:8080/privateapps/generic/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nOnce the authentication module and the new container created, we can define the authentication module on the service.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n\nNow you can try to call your route and see the login module appears.\n\n> \n\nThe allow access to the user.\n\n> \n\nIf you had any errors, make sure of :\n\n* check if you are on http or https, and if the **secure cookie option** is enabled or not on the authentication module\n* check if your OAuth1 server has the REDIRECT_URI set on **privateapps/...**\n* Make sure your server supports POST or GET OAuth1 flow set on authentication module\n\nOnce the configuration is working, you can check, when connecting with an Otoroshi admin user, the `Private App session` created (use the cog at the top right of the page, and select `Priv. app sesssions`, or navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) { open=new }).\n\nOne interesing feature is to check the profile of the connected user. In our case, when clicking on the `Profile` button of the right user, we should have : \n\n```json\n{\n \"email\": \"foo@example.com\",\n \"id\": 1,\n \"name\": \"test name\",\n \"screen_name\": \"example-user\"\n}\n```"},{"name":"secure-with-oauth2-client-credentials.md","id":"/how-to-s/secure-with-oauth2-client-credentials.md","url":"/how-to-s/secure-with-oauth2-client-credentials.html","title":"Secure an app with OAuth2 client_credential flow","content":"# Secure an app with OAuth2 client_credential flow\n\n\n\nOtoroshi makes it easy for your app to implement the [OAuth2 Client Credentials Flow](https://auth0.com/docs/authorization/flows/client-credentials-flow). \n\nWith machine-to-machine (M2M) applications, the system authenticates and authorizes the app rather than a user. With the client credential flow, applications will pass along their Client ID and Client Secret to authenticate themselves and get a token.\n\n## Deployed the Client Credential Service\n\nThe Client Credential Service must be enabled as a global plugin on your Otoroshi instance. Once enabled, it will expose three endpoints to issue and validate tokens for your routes.\n\nLet's navigate to your otoroshi instance (in our case http://otoroshi.oto.tools:8080) on the danger zone (`top right cog icon / Danger zone` or at [/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone)).\n\nTo enable a plugin in global on Otoroshi, you must add it in the `Global Plugins` section.\n\n1. Open the `Global Plugin` section \n2. Click on `enabled` (if not already done)\n3. Search the plugin named `Client Credential Service` of type `Sink` (you need to enabled it on the old or new Otoroshi engine, depending on your use case)\n4. Inject the default configuration by clicking on the button (if you are using the old Otoroshi engine)\n\nIf you click on the arrow near each plugin, you will have the documentation of the plugin and its default configuration.\n\nThe client credential plugin has by default 4 parameters : \n\n* `domain`: a regex used to expose the three endpoints (`default`: *)\n* `expiration`: duration until the token expire (in ms) (`default`: 3600000)\n* `defaultKeyPair`: a key pair used to sign the jwt token. By default, Otoroshi is deployed with an otoroshi-jwt-signing that you can visualize on the jwt verifiers certificates (`default`: \"otoroshi-jwt-signing\")\n* `secure`: if enabled, Otoroshi will expose routes only in the https requests case (`default`: true)\n\nIn this tutorial, we will set the configuration as following : \n\n* `domain`: oauth.oto.tools\n* `expiration`: 3600000\n* `defaultKeyPair`: otoroshi-jwt-signing\n* `secure`: false\n\nNow that the plugin is running, third routes are exposed on each matching domain of the regex.\n\n* `GET /.well-known/otoroshi/oauth/jwks.json` : retrieve all public keys presents in Otoroshi\n* `POST /.well-known/otoroshi/oauth/token/introspect` : validate and decode the token \n* `POST /.well-known/otoroshi/oauth/token` : generate a token with the fields provided\n\nOnce the global configuration saved, we can deployed a simple service to test it.\n\nLet's navigate to the routes page, and create a new route with : \n\n1. `foo.oto.tools` as `domain` in the frontend node\n2. `mirror.otoroshi.io` as hostname in the list of targets of the backend node, and `443` as `port`.\n3. Search in the list of plugins and add the `Apikeys` plugin to the flow\n4. In the extractors section of the `Apikeys` plugin, disabled the `Basic`, `Client id` and `Custom headers` option.\n5. Save your route\n\nLet's make a first call, to check if the jwks are already exposed :\n\n```sh\ncurl 'http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json'\n```\n\nThe output should look like a list of public keys : \n```sh\n{\n \"keys\": [\n {\n \"kty\": \"RSA\",\n \"e\": \"AQAB\",\n \"kid\": \"otoroshi-intermediate-ca\",\n ...\n }\n ...\n ]\n}\n``` \n\nLet's make a call to your route. \n\n```sh\ncurl 'http://foo.oto.tools:8080/'\n```\n\nThis should output the expected error: \n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nThe first step is to generate an api key. Navigate to the api keys page, and create an item with the following values (it will be more easy to use them in the next step)\n\n* `my-id` as `ApiKey Id`\n* `my-secret` as `ApiKey Secret`\n\nThe next step is to get a token by calling the endpoint `http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json`. The required fields are the grand type, the client and the client secret corresponding to our generated api key.\n\n```sh\ncurl -X POST http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/token \\\n-H \"Content-Type: application/json\" \\\n-d @- <<'EOF'\n{\n \"grant_type\": \"client_credentials\",\n \"client_id\":\"my-id\",\n \"client_secret\":\"my-secret\"\n}\nEOF\n```\n\nThis request have one more optional field, named `scope`. The scope can be used to set a bunch of scope on the generated access token.\n\nThe last command should look like : \n\n```sh\n{\n \"access_token\": \"generated-token-xxxxx\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600\n}\n```\n\nNow we can call our api with the generated token\n\n```sh\ncurl 'http://foo.oto.tools:8080/' \\\n -H \"Authorization: Bearer generated-token-xxxxx\"\n```\n\nThis should output a successful call with the list of headers with a field named `Authorization` containing the previous access token.\n\n## Other possible configuration\n\nBy default, Otoroshi generate the access token with the specified key pair in the configuration. But, in some case, you want a specific key pair by client_id/client_secret.\nThe `jwt-sign-keypair` metadata can be set on any api key with the id of the key pair as value. \n"},{"name":"setup-otoroshi-cluster.md","id":"/how-to-s/setup-otoroshi-cluster.md","url":"/how-to-s/setup-otoroshi-cluster.html","title":"Setup an Otoroshi cluster","content":"# Setup an Otoroshi cluster\n\n
\nRoute plugins:\nAdditional Headers In\n
\n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n"},{"name":"tailscale-integration.md","id":"/how-to-s/tailscale-integration.md","url":"/how-to-s/tailscale-integration.html","title":"Tailscale integration","content":"# Tailscale integration\n\n[Tailscale](https://tailscale.com/) is a VPN service that let you create your own private network based on [Wireguard](https://www.wireguard.com/). Tailscale goes beyond the simple meshed wireguard based VPN and offers out of the box NAT traversal, third party identity provider integration, access control, magic DNS and let's encrypt integration for the machines on your VPN.\n\nOtoroshi provides somes plugins out of the box to work in a [Tailscale](https://tailscale.com/) environment.\n\nby default Otoroshi, works out of the box when integrated in a `tailnet` as you can contact other machines usign their ip address. But we can go a little bit further.\n\n## tailnet configuration\n\nfirst thing, go to your tailnet setting on [tailscale.com](https://login.tailscale.com/admin/machines) and go to the [DNS tab](https://login.tailscale.com/admin/dns). Here you can find \n\n* your tailnet name: the domain name of all your machines on your tailnet\n* MagicDNS: a way to address your machines by directly using their names\n* HTTPS Certificates: HTTPS certificates provision for all your machines\n\nto use otoroshi Tailscale plugin you must enable `MagicDNS` and `HTTPS Certificates`\n\n## Tailscale certificates integration\n\nyou can use tailscale generated let's encrypt certificates in otoroshi by using the `Tailscale certificate fetcher job` in the plugins section of the danger zone. Once enabled, this job will fetch certificates for domains in `xxxx.ts.net` that belong to your tailnet. \n\nas usual, the fetched certificates will be available in the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) of otoroshi.\n\n## Tailscale targets integration\n\nthe following pair of plugins let your contact tailscale machine by using their names even if their are multiple instance.\n\nwhen you register a machine on a tailnet, you have to provide a name for it, let say `my-server`. This machine will be addressable in your tailnet with `my-server.tailxxx.ts.net`. But if you have multiple instance of the same server on several machines with the same `my-server` name, their DNS name on the tailnet will be `my-server.tailxxx.ts.net`, `my-server-1.tailxxx.ts.net`, `my-server-2.tailxxx.ts.net`, etc. If you want to use those names in an otoroshi backend it could be tricky if the application has something like autoscaling enabled.\n\nin that case, you can add the `Tailscale targets job` in the plugins section of the danger zone. Once enabled, this job will fetch periodically available machine on the tailnet with their names and DNS names. Then, in a route, you can use the `Tailscale select target by name` plugin to tell otoroshi to loadbalance traffic between all machine that have the name specified in the plugin config. instead of their DNS name."},{"name":"tls-termination-using-own-certificates.md","id":"/how-to-s/tls-termination-using-own-certificates.md","url":"/how-to-s/tls-termination-using-own-certificates.html","title":"TLS termination using your own certificates","content":"# TLS termination using your own certificates\n\nThe goal of this tutorial is to expose a service via https using a certificate generated by openssl.\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nTry to call the service.\n\n```sh\ncurl 'http://myservice.oto.tools:8080'\n```\n\nThis should output something like\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463145856319359618\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8080\",\n \"opun-gateway-request-id\": \"1463145856554240100\",\n \"x-forwarded-proto\": \"https\",\n },\n \"body\": \"\"\n}\n```\n\nLet's try to call the service in https.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nThis should output\n\n```sh\ncurl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to myservice.oto.tools:8443\n```\n\nTo fix it, we have to generate a certificate and import it in Otoroshi to match the domain `myservice.oto.tools`.\n\n> If you already had a certificate you can skip the next set of commands and directly import your certificate in Otoroshi\n\nWe will use openssl to generate a private key and a self-signed certificate.\n\n```sh\nopenssl genrsa -out myservice.key 4096\n# remove pass phrase\nopenssl rsa -in myservice.key -out myservice.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key myservice.key -out myservice.cer -subj \"/CN=myservice.oto.tools\"\n```\n\nCheck the content of the certificate \n\n```sh\nopenssl x509 -in myservice.cer -text\n```\n\nThis should contains something like\n\n```sh\nCertificate:\n Data:\n Version: 1 (0x0)\n Serial Number: 9572962808320067790 (0x84d9fef455f188ce)\n Signature Algorithm: sha256WithRSAEncryption\n Issuer: CN=myservice.oto.tools\n Validity\n Not Before: Nov 23 14:25:55 2021 GMT\n Not After : Nov 23 14:25:55 2022 GMT\n Subject: CN=myservice.oto.tools\n Subject Public Key Info:\n Public Key Algorithm: rsaEncryption\n Public-Key: (4096 bit)\n Modulus:\n...\n```\n\nOnce generated, go back to Otoroshi and navigate to the certificates management page (`top right cog icon / SSL/TLS certificates` or at @link:[`/bo/dashboard/certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates)) and click on `Add item`.\n\nSet `myservice-certificate` as `name` and `description`.\n\nDrop the `myservice.cer` file or copy the content to the `Certificate full chain` field.\n\nDo the same action for the `myservice.key` file in the `Certificate private key` field.\n\nSet your passphrase password in the `private key password` field if you added one.\n\nLet's try the same call to the service.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nAn error should occurs due to the untrsuted received certificate server\n\n```sh\ncurl: (60) SSL certificate problem: self signed certificate\nMore details here: https://curl.haxx.se/docs/sslcerts.html\n\ncurl failed to verify the legitimacy of the server and therefore could not\nestablish a secure connection to it. To learn more about this situation and\nhow to fix it, please visit the web page mentioned above.\n```\n\nEnd this tutorial by trusting the certificate server \n\n```sh\ncurl 'https://myservice.oto.tools:8443' --cacert myservice.cer\n```\n\nThis should finally output\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463158439730479893\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8443\",\n \"opun-gateway-request-id\": \"1463158439558515871\",\n \"x-forwarded-proto\": \"https\",\n \"sozu-id\": \"01FN6MGKSYZNJYHEMP4R5PJ4Q5\"\n },\n \"body\": \"\"\n}\n```\n\n"},{"name":"tls-using-lets-encrypt.md","id":"/how-to-s/tls-using-lets-encrypt.md","url":"/how-to-s/tls-using-lets-encrypt.html","title":"TLS termination using Let's Encrypt","content":"# TLS termination using Let's Encrypt\n\nAs you know, Otoroshi is capable of doing TLS termination for your services. You can import your own certificates, generate certificates from scratch and you can also use the @link:[ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555) to generate certificates. One of the most popular service offering ACME certificates creation is @link:[Let's Encrypt](https://letsencrypt.org/).\n\n@@@ warning\nIn order to make this tutorial work, your otoroshi instance MUST be accessible from the internet in order to be reachable by Let's Encrypt ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n@@@ note\nthis tutorial can work with any ACME provider with the same rules. your otoroshi instance MUST be accessible by the ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n## Setup let's encrypt on otoroshi\n\nGo on the danger zone page by clicking on the [`cog icon / Danger Zone`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Scroll to the `Let's Encrypt settings` section. Enable it, and specify the address of the ACME server (for production Let's Encrypt it's `acme://letsencrypt.org`, for testing, it's `acme://letsencrypt.org/staging`. Any ACME server address should work). You can also add one or more email addresses or contact urls that will be included in your Let's Encrypt account. You don't have to fill the `public/private key` inputs as they will be automatically generated on the first usage.\n\n## Creating let's encrypt certificate from FQDNs\n\nYou can go to the certificates page by clicking on the [`cog icon / SSL/TLS Certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Here, click on the `+ Let's Encrypt certificate` button. A popup will show up to ask you the FQDN that you want for you certificate. Once done, click on the `Create` button. A few moment later, you will be redirected on a brand new certificate generated by Let's encrypt. You can now enjoy accessing your service behind the FQDN with TLS.\n\n## Creating let's encrypt certificate from a service\n\nYou can go to any service page and enable the flag `Issue Let's Encrypt cert.`. Do not forget to save your service. A few moment later, the certificates will be available in the certificates page and you can will be able to enjoy accessing your service with TLS.\n"},{"name":"wasm-usage.md","id":"/how-to-s/wasm-usage.md","url":"/how-to-s/wasm-usage.html","title":"Using wasm plugins","content":"# Using wasm plugins\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. You can find more about those plugins @ref:[here](../topics/wasm-usage.md)\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create the route with the plugin validator](#create-the-route-with-the-plugin-validator)\n3. [Test your validator](#test-your-validator)\n4. [Update the route by replacing the backend with a WASM file](#update-the-route-by-replacing-the-backend-with-a-wasm-file)\n5. [WASM backend test](#wasm-backend-test)\n6. [Expose a single file as WASM backend](#expose-a-single-file-as-wasm-backend)\n\nAfter completing these steps you will have a route that uses WASM plugins written in Rust.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create the route with the plugin validator\n\nFor this tutorial, we will start with an existing wasm file. The main function of this file will check the value of an http header to allow access or not. The can find this file at [https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm)\n\nThe main function of this validator, written in rust, should look like:\n\nvalidator.rs\n: @@snip [validator.rs](../snippets/wasmo/validator.rs) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.ts\n: @@snip [validator.ts](../snippets/wasmo/validator.ts) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.go\n: @@snip [validator.js](../snippets/wasmo/validator.go) \n\nThe plugin receives the request context from Otoroshi (the matching route, the api key if present, the headers, etc) as `WasmAccessValidatorContext` object. \nThen it applies a check on the headers, and responds with an error or success depending on the content of the foo header. \nObviously, the previous snippet is an example and the editor allows you to write whatever you want as a check.\n\nLet's create a route that uses the previous wasm file as an access validator plugin :\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThis request will apply the following process:\n\n* names the route *demo-otoroshi*\n* creates a frontend exposed on the `demo-otoroshi.oto.tools` \n* forward requests on one target, reachable at `mirror.otoroshi.io` using TLS on port 443\n* adds the *WasmAccessValidator* plugin to validate access based on the foo header to the route\n\nYou can validate the route creation by navigating to the [dashboard](http://otoroshi.oto.tools:8080/bo/dashboard/routes/demo-otoroshi?tab=flow)\n\n## Test your validator\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -I\n```\n\nThis should output the following error:\n\n```\nHTTP/1.1 401 Unauthorized\n```\n\nLet's call again the route by adding the header foo with the bar value.\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -I\n```\n\nThis should output the successfull message:\n\n```\nHTTP/1.1 200 OK\n```\n\n## Update the route by replacing the backend with a WASM file\n\nThe next step in this tutorial is to use a WASM file as backend of the route. We will use an existing WASM file, available in our wasm demos repository on github. \nThe content of this plugin, called `wasm-target.wasm`, looks like:\n\ntarget.rs\n: @@snip [target.rs](../snippets/wasmo/target.rs) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.ts\n: @@snip [target.ts](../snippets/wasmo/target.ts) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.go\n: @@snip [target.js](../snippets/wasmo/target.go) \n\nLet's explain this snippet. The purpose of this type of plugin is to respond an HTTP response with http status, body and headers map.\n\n1. Includes all public structures from `types.rs` file. This file contains predefined Otoroshi structures that plugins can manipulate.\n2. Necessary imports. [Extism](https://extism.org/docs/overview)'s goal is to make all software programmable by providing a plug-in system. \n3. Creates a map of new headers that will be merged with incoming request headers.\n4. Creates the response object with the map of merged headers, a simple JSON body and a successfull status code.\n\nThe file is downloadable [here](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm).\n\nLet's update the route using the this wasm file.\n\n```sh\ncurl -X PUT \"http://otoroshi-api.oto.tools:8080/api/routes/demo-otoroshi\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmBackend\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThe response should contains the updated route content.\n\n## WASM backend test\n\nLet's call our route.\n\n```sh\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -H \"fifi: foo\" -v\n```\n\nThis should output:\n\n```\n* Trying 127.0.0.1:8080...\n* Connected to demo-otoroshi.oto.tools (127.0.0.1) port 8080 (#0)\n> GET / HTTP/1.1\n> Host: demo-otoroshi.oto.tools:8080\n> User-Agent: curl/7.79.1\n> Accept: */*\n> foo:bar\n> fifi:foo\n>\n* Mark bundle as not supporting multiuse\n< HTTP/1.1 200 OK\n< foo: bar\n< Host: demo-otoroshi.oto.tools:8080\n<\n* Closing connection 0\n{\"foo\": \"bar\"}\n```\n\nIn this response, we can find our headers send in the curl command and those added by the wasm plugin.\n\n\n## Expose a single file as WASM backend\n\nA WASM backend plugin can directly expose a file written in Wasmo. This is the simplest possibility to write HTML, Javascript, JSON or expose a simple PNG image.\n\nLet's expose a HTML page. In your Wasmo instance, execute the following instructions:\n\n1. Click the new plugin button\n2. Add a name and `validate`\n3. Click the new plugin\n4. Create a new file named `index.html`\n5. Copy and paste the following content\n\n```html\n \n\n\n Wasmo plugin\n\n\n

Hello from Wasmo

\n\n\n```\n\nThis snippet is a short HTML template with a title to indicate that it comes from Wasmo. \n\nNow we can write our javascript function to parse and return the content of our HTML to Otoroshi. \n\n1. Navigate to the `index.js` file\n2. Replace the content with the following content\n\n```js\nimport IndexPage from './index.html'\n\nexport function execute() {\n \n let response = {\n headers: {\n 'Content-Type': 'text/html; charset=utf-8'\n },\n body: IndexPage,\n status: 200\n };\n \n Host.outputString(JSON.stringify(response));\n\n return 0;\n}\n```\n\nThe code is pretty self-explanatory. We start by importing our HTML page and we build the response with the correct content type, the body and a 200 http status.\n\nJust before testing, we need to change the esbuild configuration to specify how to bundle the HTML file.\n\nThe contents of your `esbuild.js` file should look this:\n\n```js\nconst esbuild = require('esbuild');\n\nesbuild\n .build({\n entryPoints: ['index.js'],\n outdir: 'dist',\n bundle: true,\n loader: {\n '.html': 'text'\n },\n sourcemap: true,\n minify: false, // might want to use true for production build\n format: 'cjs', // needs to be CJS for now\n target: ['es2020'] // don't go over es2020 because quickjs doesn't support it\n })\n```\n\nCheck your browser at `http://demo-otoroshi.oto.tools:8080` and you should see your page content updated to the new text.\n\nIf you need to expose more than a HTML page, we highly recommend to use the @ref:[Zip Backend plugin](../how-to-s/zip-backend-plugin.md)"},{"name":"wasmo-installation.md","id":"/how-to-s/wasmo-installation.md","url":"/how-to-s/wasmo-installation.html","title":"Deploy your own Wasmo","content":"# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: admin-api-apikey-id\n* `Apikey id`: admin-api-apikey-secret\n* `Apikey secret`: http://localhost:5001\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n"},{"name":"working-with-eureka.md","id":"/how-to-s/working-with-eureka.md","url":"/how-to-s/working-with-eureka.html","title":"Working with Eureka","content":"# Working with Eureka\n\n\n\nEureka is a library of Spring Cloud Netflix, which provides two parts to register and discover services.\nGenerally, the services are applications written with Spring but Eureka also provides a way to communicate in REST. The main goals of Eureka are to allow clients to find and communicate with each other without hard-coding the hostname and port.\nAll services are registered in an Eureka Server.\n\nTo work with Eureka, Otoroshi has three differents plugins:\n\n* to expose its own Eureka Server instance\n* to discover an existing Eureka Server instance\n* to use Eureka application as an Otoroshi target and took advantage of all Otoroshi clients features (load-balancing, rate limiting, etc...)\n\nLet's cut this tutorial in three parts. \n\n- Create an simple Spring application that we'll use as an Eureka Client\n- Deploy an implementation of the Otoroshi Eureka Server (using the `Eureka Instance` plugin), register eureka clients and expose them using the `Internal Eureka Server` plugin\n- Deploy an Netflix Eureka Server and use it in Otoroshi to discover apps using the `External Eureka Server` plugin.\n\n\nIn this tutorial: \n\n- [Create an Otoroshi route with the Internal Eureka Server plugin](#create-an-otoroshi-route-with-the-internal-eureka-server-plugin)\n- [Create a simple Eureka Client and register it](#create-a-simple-eureka-client-and-register-it)\n- [Connect to an external Eureka server](#connect-to-an-external-eureka-server)\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create an Otoroshi route with the Internal Eureka Server plugin\n\n@@@ note\nWe'll supposed that you have an Otoroshi exposed on the 8080 port with the new Otoroshi engine enabled\n@@@\n\nLet's jump to the routes Otoroshi [view](http://otoroshi.oto.tools:8080/bo/dashboard/routes) and create a new route using the wizard button.\n\nEnter the following values in for each step:\n\n1. An Eureka Server instance\n2. Choose the first choice : **BLANK ROUTE** and click on continue\n3. As exposed domain, set `eureka-server.oto.tools/eureka`\n4. As Target URL, set `http://foo.bar` (this value has no importance and will be skip by the Otoroshi Instance plugin)\n5. Validate the creation\n\nOnce created, you can hide with the arrow on the right top of the screen the tester view (which is displayed by default after each route creation).\nIn our case, we want to add a new plugin, called Internal Eureka Instance on our feed.\n\nInside the designer view:\n\n1. Search the `Eureka Instance` in the list of plugins.\n2. Add it to the feed by clicking on it\n3. Set an eviction timeout at 300 seconds (this configuration is used by Otoroshi to automatically check if an Eureka is up. Otherwise Otoroshi will evict the eureka client from the registry)\n\nWell done you have set up an Eureka Server. To check the content of an Eureka Server, you can navigate to this [link]('http://otoroshi.oto.tools:8080/bo/dashboard/eureka-servers'). In all case, none instances or applications are registered, so the registry is currently empty.\n\n### Create a simple Eureka Client and register it\n\n*This tutorial has no vocation to teach you how to write an Spring application and it may exists a newer version of this Spring code.*\n\n\nFor this tutorial, we'll use the following code which initiates an Eureka Client and defines an Spring REST Controller with only one endpoint. This endpoint will return its own exposed port (this value will be useful to check that the Otoroshi load balancing is right working between the multiples Eureka instances registered).\n\n\nLet's fast create a Spring project using [Spring Initializer](https://start.spring.io/). You can use the previous link or directly click on the following link to get the form already filled with the needed dependencies.\n\n````bash\nhttps://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.3&packaging=jar&jvmVersion=17&groupId=otoroshi.io&artifactId=eureka-client&name=eureka-client&description=A%20simple%20eureka%20client&packageName=otoroshi.io.eureka-client&dependencies=cloud-eureka,web\n````\n\nFeel free to change the project metadata for your use case.\n\nOnce downloaded and uncompressed, let's ahead and start to delete the application.properties and create an application.yml (if you are more comfortable with an application.properties, keep it)\n\n````yaml\neureka:\n client:\n fetch-registry: false # disable the discovery services mechanism for the client\n serviceUrl:\n defaultZone: http://eureka-server.oto.tools:8080/eureka\n\nspring:\n application:\n name: foo_app\n\n````\n\n\nNow, let's define the simple REST controller to expose the client port.\n\nCreate a new file, called PortController.java, in the sources folder of your project with the following content.\n\n````java\npackage otoroshi.io.eurekaclient;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.env.Environment;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class PortController {\n\n @Autowired\n Environment environment;\n\n @GetMapping(\"/port\")\n public String index() {\n return environment.getProperty(\"local.server.port\");\n }\n}\n````\nThis controller is very simple, we just exposed one endpoint `/port` which returns the port as string. Our client is ready to running. \n\nLet's launch it with the following command:\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085\n````\n\n@@@note\nThe port is not required but it will be useful when we will deploy more than one instances in the rest of the tutorial\n@@@\n\n\nOnce the command ran, you can navigate to the eureka server view in the Otoroshi UI. The dashboard should displays one registered app and instance.\nIt should also displays a timer for each application which represents the elapsed time since the last received heartbeat.\n\nLet's define a new route to exposed our registered eureka client.\n\n* Create a new route, named `Eureka client`, exposed on `http://eureka-client.oto.tools:8080` and targeting `http://foo.bar`\n* Search and add the `Internal Eureka server` plugin \n* Edit the plugin and choose your eureka server and your app (in our case, `Eureka Server` and `FOO_APP` respectively)\n* Save your route\n\nNow try to call the new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n````\n\nIf everything is working, you should get the port 8085 as the response.The setup is working as expected, but we can improve him by scaling our eureka client.\n\nOpen a new tab in your terminal and run the following command.\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8083\n````\n\nJust wait a few seconds and retry to call your new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\n````\n\nThe configuration is ready and the setup is working, Otoroshi use all instances of your app to dispatch clients on it.\n\n### Connect to an external Eureka server\n\nOtoroshi has the possibility to discover services by connecting to an Eureka Server.\n\nLet's create a route with an Eureka application as Otoroshi target:\n\n* Create a new blank API route\n* Search and add the `External Eureka Server` plugin\n* Set your eureka URL\n* Click on `Fetch Services` button to discover the applications of the Eureka instance\n* In the appeared selector, choose the application to target\n* Once the frontend configured, save your route and try to call it.\n\nWell done, you have exposed your Eureka application through the Otoroshi discovery services.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"name":"zip-backend-plugin.md","id":"/how-to-s/zip-backend-plugin.md","url":"/how-to-s/zip-backend-plugin.html","title":"Quickly expose a website and static files ","content":"# Quickly expose a website and static files \n\n@@include[badge.md](../includes/badge.md) { #badge }\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create an archive with HTML and CSS files](#create-an-archive-with-html-and-css-files)\n2. [Use the Zip Backend Plugin](#use-the-zip-backend-plugin)\n\nAfter completing these steps, you will be able to statically expose any kind of files from an archive.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create an archive with HTML and CSS files\n\nLet's start by creating an archive composed of html and css files.\n\nThe contents of your `index.html` file should be likes this:\n\n```html\n\n\n\n Wasmo plugin\n \n\n\n

Hello from Wasmo

\n\n\n```\n\nThe contents of your `index.css` file should be likes this:\n\n```css\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nOnce created, you can create the archive of both.\n\n```sh\nzip bundle.zip index.html index.css\n```\n\n## Use the Zip Backend Plugin \n\nLet's create the route using the Otoroshi admin API. The route content is pretty simple, a few fields about the name and the frontend, and the Zip Backend plugin in the plugins list.\n\nDon't forget to change the default `path-to-the-zip-file` with your path.\n\n``` sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"demootoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"debug\": false,\n \"plugin\": \"cp:otoroshi.next.plugins.ZipFileBackend\",\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"url\": \"file:///bundle.zip\",\n \"headers\": {},\n \"dir\": \"./zips\",\n \"prefix\": null,\n \"ttl\": 3600000\n }\n }\n ]\n}\nEOF\n```\n\nCalling the route in a new browser tab at `http://demo-otoroshi.oto.tools:8080/`. You should see something like the following image:\n\n@@@ div { .centered-img }\n\n@@@\n\nAs we can see, the content of the archive is available, our HTML page is served and the CSS, linked into the HTML page, has loaded.\n\nYou can check this behaviour by calling the following path: \n\n```bash\ncurl http://demo-otoroshi.oto.tools:8080/index.css -v\n```\n\nThe result should be like:\n\n```bash\n< HTTP/1.1 200 OK\n< Transfer-Encoding: chunked\n< Content-Type: text/css\n<\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nCongratulations - You have just exposed your first archive. Do not hesitate to expose any type of content."},{"name":"badge.md","id":"/includes/badge.md","url":"/includes/badge.html","title":"","content":"\n
\nRoute plugins:\n
Zip file backend plugin
\n
\n\n"},{"name":"experimental.md","id":"/includes/experimental.md","url":"/includes/experimental.html","title":"@@@ warning","content":"@@@ warning\n\nthis feature is **EXPERIMENTAL** and might not work as expected.
\nIf you encounter any bugs, [please fill an issue](https://github.com/MAIF/otoroshi/issues/new), it will help us a lot :)\n\n@@@\n"},{"name":"fetch-and-start.md","id":"/includes/fetch-and-start.md","url":"/includes/fetch-and-start.html","title":"","content":"\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n"},{"name":"initialize.md","id":"/includes/initialize.md","url":"/includes/initialize.html","title":"","content":"\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
\nSet up an Otoroshi\n\n
\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n"},{"name":"index.md","id":"/index.md","url":"/index.html","title":"Otoroshi","content":"# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.14.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n"},{"name":"get-otoroshi.md","id":"/install/get-otoroshi.md","url":"/install/get-otoroshi.html","title":"Get Otoroshi","content":"# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi-16.14.0-dev.zip\nunzip ./otoroshi-16.14.0-dev.zip\ncd otoroshi-16.14.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.14.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n"},{"name":"index.md","id":"/install/index.md","url":"/install/index.html","title":"Install","content":"# Install\n\nIn this sections, you will find informations about how to install and run Otoroshi\n\n* @ref:[Get Otoroshi](./get-otoroshi.md)\n* @ref:[Setup Otoroshi](./setup-otoroshi.md)\n* @ref:[Run Otoroshi](./run-otoroshi.md)\n\n@@@ index\n\n* [Get Otoroshi](./get-otoroshi.md)\n* [Setup Otoroshi](./setup-otoroshi.md)\n* [Run Otoroshi](./run-otoroshi.md)\n\n@@@\n"},{"name":"run-otoroshi.md","id":"/install/run-otoroshi.md","url":"/install/run-otoroshi.html","title":"Run Otoroshi","content":"# Run Otoroshi\n\nNow you are ready to run Otoroshi. You can run the following command with some tweaks depending on the way you want to configure Otoroshi. If you want to pass a custom configuration file, use the `-Dconfig.file=/path/to/file.conf` flag in the following commands.\n\n## From .zip file\n\n```sh\ncd otoroshi-vx.x.x\n./bin/otoroshi\n```\n\n## From .jar file\n\nFor Java 11\n\n```sh\njava -jar otoroshi.jar\n```\n\nif you want to run the jar file for on a JDK above JDK11, you'll have to add the following flags\n\n```sh\njava \\\n --add-opens=java.base/javax.net.ssl=ALL-UNNAMED \\\n --add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED \\\n --add-exports=java.base/sun.security.x509=ALL-UNNAMED \\\n --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \\\n -Dlog4j2.formatMsgNoLookups=true \\\n -jar otoroshi.jar\n```\n\n## From docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi\n```\n\nYou can also pass useful args like :\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi -Dconfig.file=/usr/app/otoroshi/conf/otoroshi.conf -Dlogger.file=/usr/app/otoroshi/conf/otoroshi.xml\n```\n\nIf you want to provide your own config file, you can read @ref:[the documentation about config files](./setup-otoroshi.md).\n\nYou can also provide some ENV variable using the `--env` flag to customize your Otoroshi instance.\n\nThe list of possible env variables is available @ref:[here](./setup-otoroshi.md).\n\nYou can use a volume to provide configuration like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/conf\" maif/otoroshi\n```\n\nYou can also use a volume if you choose to use `filedb` datastore like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd)/filedb:/usr/app/otoroshi/filedb\" maif/otoroshi -Dotoroshi.storage=file\n```\n\nYou can also use a volume if you choose to use exports files :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/imports\" maif/otoroshi -Dotoroshi.importFrom=/usr/app/otoroshi/imports/export.json\n```\n\n## Run examples\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -Dotoroshi.importFrom=/home/user/otoroshi.json \\\n -Dconfig.file=/home/user/otoroshi.conf \\\n -jar ./otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - Importing from: /home/user/otoroshi.json\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n\nIf you choose to start Otoroshi without importing existing data, Otoroshi will create a new admin user and print the login details in the log. When you will log into the admin dashboard, Otoroshi will ask you to create another account to avoid security issues.\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -jar otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / HHUsiF2UC3OPdmg0lGngEv3RrbIwWV5W\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n"},{"name":"setup-otoroshi.md","id":"/install/setup-otoroshi.md","url":"/install/setup-otoroshi.html","title":"Setup Otoroshi","content":"# Setup Otoroshi\n\nin this section we are going to configure otoroshi before running it for the first time\n\n## Setup the database\n\nRight now, Otoroshi supports multiple datastore. You can choose one datastore over another depending on your use case.\n\n@@@div { .plugin .platform } \n
Redis
\n\n
Recommended
\n\nThe **redis** datastore is quite nice when you want to easily deploy several Otoroshi instances.\n\n\n\n@link:[Documentation](https://redis.io/topics/quickstart)\n@@@\n\n@@@div { .plugin .platform } \n
In memory
\n\nThe **in-memory** datastore is kind of interesting. It can be used for testing purposes, but it is also a good candidate for production because of its fastness.\n\n\n\n@ref:[Start with](../getting-started.md)\n@@@\n\n@@@div { .plugin .platform } \n
Cassandra
\n\n
Clustering
\n\nExperimental support, should be used in cluster mode for leaders\n\n\n\n@link:[Documentation](https://cassandra.apache.org/doc/latest/cassandra/getting_started/installing.html)\n@@@\n\n@@@div { .plugin .platform } \n
Postgresql
\n\n
Clustering
\n\nOr any postgresql compatible databse like cockroachdb for instance (experimental support, should be used in cluster mode for leaders)\n\n\n\n@link:[Documentation](https://www.postgresql.org/docs/10/tutorial-install.html)\n@@@\n\n@@@div { .plugin .platform } \n\n
FileDB
\n\nThe **filedb** datastore is pretty handy for testing purposes, but is not supposed to be used in production mode. \nNot suitable for production usage.\n\n\n\n@@@\n\n\n@@@ div { .centered-img }\n\n@@@\n\nthe first thing to setup is what kind of datastore you want to use with the `otoroshi.storage` setting\n\n```conf\notoroshi {\n storage = \"inmemory\" # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n}\n```\n\ndepending on the value you chose, you will be able to configure your datastore with the following configuration\n\ninmemory\n: @@snip [inmemory.conf](../snippets/datastores/inmemory.conf) \n\nfile\n: @@snip [file.conf](../snippets/datastores/file.conf) \n\nhttp\n: @@snip [http.conf](../snippets/datastores/http.conf) \n\ns3\n: @@snip [s3.conf](../snippets/datastores/s3.conf) \n\nredis\n: @@snip [lettuce.conf](../snippets/datastores/lettuce.conf) \n\npostgresql\n: @@snip [pg.conf](../snippets/datastores/pg.conf) \n\ncassandra\n: @@snip [inmemory.conf](../snippets/datastores/cassandra.conf) \n\n## Setup your hosts before running\n\nBy default, Otoroshi starts with domain `oto.tools` that automatically targets `127.0.0.1` with no changes to your `/etc/hosts` file. Of course you can change the domain value, you have to add the values in your `/etc/hosts` file according to the setting you put in Otoroshi configuration or define the right ip address at the DNS provider level\n\n* `otoroshi.domain` => `mydomain.org`\n* `otoroshi.backoffice.subdomain` => `otoroshi`\n* `otoroshi.privateapps.subdomain` => `privateapps`\n* `otoroshi.adminapi.exposedSubdomain` => `otoroshi-api`\n* `otoroshi.adminapi.targetSubdomain` => `otoroshi-admin-internal-api`\n\nfor instance if you want to change the default domain and use something like `otoroshi.mydomain.org`, then start otoroshi like \n\n```sh\njava -Dotoroshi.domain=mydomain.org -jar otoroshi.jar\n```\n\n@@@ warning\nOtoroshi cannot be accessed using `http://127.0.0.1:8080` or `http://localhost:8080` because Otoroshi uses Otoroshi to serve it's own UI and API. When otoroshi starts with an empty database, it will create a service descriptor for that using `otoroshi.domain` and the settings listed on this page and in the here that serve Otoroshi API and UI on `http://otoroshi-api.${otoroshi.domain}` and `http://otoroshi.${otoroshi.domain}`.\nOnce the descriptor is saved in database, if you want to change `otoroshi.domain`, you'll have to edit the descriptor in the database or restart Otoroshi with an empty database.\n@@@\n\n@@@ warning\nif your otoroshi instance runs behind a reverse proxy (L4 / L7) or inside a docker container where exposed ports (that you will use to access otoroshi) are not the same that the ones configured in otoroshi (`http.port` and `https.port`), you'll have to configure otoroshi exposed port to avoid bad redirection URLs when using authentication modules and other otoroshi tools. To do that, just set the values of the exposed ports in `otoroshi.exposed-ports.http = $theExposedHttpPort` (OTOROSHI_EXPOSED_PORTS_HTTP) and `otoroshi.exposed-ports.https = $theExposedHttpsPort` (OTOROSHI_EXPOSED_PORTS_HTTPS)\n@@@\n\n## Setup your configuration file\n\nThere is a lot of things you can configure in Otoroshi. By default, Otoroshi provides a configuration that should be enough for testing purpose. But you'll likely need to update this configuration when you'll need to move into production.\n\nIn this page, any configuration property can be set at runtime using a `-D` flag when launching Otoroshi like \n\n```sh\njava -Dhttp.port=8080 -jar otoroshi.jar\n```\n\nor\n\n```sh\n./bin/otoroshi -Dhttp.port=8080 \n```\n\nif you want to define your own config file and use it on an otoroshi instance, use the following flag\n\n```sh\njava -Dconfig.file=/path/to/otoroshi.conf -jar otoroshi.jar\n``` \n\n### Example of a custom. configuration file\n\n```conf\ninclude \"application.conf\"\n\nhttp.port = 8080\n\napp {\n storage = \"inmemory\"\n importFrom = \"./my-state.json\"\n env = \"prod\"\n domain = \"oto.tools\"\n rootScheme = \"http\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n privateapps {\n subdomain = \"privateapps\"\n session {\n exp = 86400000\n }\n }\n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-api-apikey-id\"\n backOfficeApiKeyClientSecret = \"admin-api-apikey-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n }\n claim {\n sharedKey = \"mysecret\"\n }\n filedb {\n path = \"./filedb/state.ndjson\"\n }\n}\n\nplay.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2592000000\n domain = \".oto.tools\"\n cookieName = \"oto-sess\"\n }\n}\n```\n\n### Reference configuration\n\n@@snip [reference.conf](../snippets/reference.conf) \n\n### More config. options\n\nSee default configuration at\n\n* @link:[Base configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/base.conf) { open=new }\n* @link:[Application configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/application.conf) { open=new }\n\n## Configuration with env. variables\n\nEevery property in the configuration file can be overriden by an environment variable if it has env variable override written like `${?ENV_VARIABLE}`).\n\n## Reference configuration for env. variables\n\n@@snip [reference-env.conf](../snippets/reference-env.conf) \n"},{"name":"built-in-legacy-plugins.md","id":"/plugins/built-in-legacy-plugins.md","url":"/plugins/built-in-legacy-plugins.html","title":"Built-in legacy plugins","content":"# Built-in legacy plugins\n\nOtoroshi provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLog }\n\n## Access log (CLF)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in CLF format.\n\nLog format is the following:\n\n`\"$service\" $clientAddress - \"$userId\" [$timestamp] \"$host $method $path $protocol\" \"$status $statusTxt\" $size $snowflake \"$to\" \"$referer\" \"$userAgent\" $http $duration $errorMsg`\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLogJson }\n\n## Access log (JSON)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in json format.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.KafkaAccessLog }\n\n## Kafka access log\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `KafkaAccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged as an event in a kafka topic.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"KafkaAccessLog\": {\n \"enabled\": true,\n \"topic\": \"otoroshi-access-log\",\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KafkaAccessLog\" : {\n \"enabled\" : true,\n \"topic\" : \"otoroshi-access-log\",\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.BasicAuthCaller }\n\n## Basic Auth. caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\nThis plugin accepts the following configuration\n\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.OAuth2Caller }\n\n## OAuth2 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\nThis plugin accepts the following configuration\n\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.cache.ResponseCache }\n\n## Response Cache\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ResponseCache\": {\n \"enabled\": true, // enabled cache\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"autoClean\": true, // cleanup older keys when all bigger than maxSize\n \"filter\": { // cache only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ResponseCache\" : {\n \"enabled\" : true,\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.clientcert.ClientCertChainHeader }\n\n## Client certificate header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ClientCertChain`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ClientCertChain\": {\n \"pem\": { // send client cert as PEM format in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-Pem\"\n },\n \"dns\": { // send JSON array of DNs in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-DNs\"\n },\n \"chain\": { // send JSON representation of client cert chain in a header\n \"send\": true,\n \"header\": \"X-Client-Cert-Chain\"\n },\n \"claims\": { // pass JSON representation of client cert chain in the otoroshi JWT token\n \"send\": false,\n \"name\": \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCertChain\" : {\n \"pem\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-Pem\"\n },\n \"dns\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-DNs\"\n },\n \"chain\" : {\n \"send\" : true,\n \"header\" : \"X-Client-Cert-Chain\"\n },\n \"claims\" : {\n \"send\" : false,\n \"name\" : \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.defer.DeferPlugin }\n\n## Defer Responses\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"DeferPlugin\": {\n \"defaultDefer\": 0 // default defer in millis\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"DeferPlugin\" : {\n \"defaultDefer\" : 0\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.discovery.DiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoHeader }\n\n## Geolocation header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `GeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfoHeader\": {\n \"headerName\": \"X-Geolocation-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfoHeader\" : {\n \"headerName\" : \"X-Geolocation-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.hmac.HMACCallerPlugin }\n\n## HMAC caller plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HMACCallerPlugin`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACCallerPlugin\" : {\n \"secret\" : \"my-defaut-secret\",\n \"algo\" : \"HMAC-SHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiCanary }\n\n## Izanami Canary Campaign\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiCanary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiProxy }\n\n## Izanami APIs Proxy\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiProxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jq.JqBodyTransformer }\n\n## JQ bodies transformer\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `JqBodyTransformer`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\nSome JSON variables are accessible by default :\n\n * `$url`: the request url\n * `$path`: the request path\n * `$domain`: the request domain\n * `$method`: the request method\n * `$headers`: the current request headers (with name in lowercase)\n * `$queryParams`: the current request query params\n * `$otoToken`: the otoroshi protocol token (if one)\n * `$inToken`: the first matched JWT token as is (from verifiers, if one)\n * `$token`: the first matched JWT token as is (from verifiers, if one)\n * `$user`: the current user (if one)\n * `$apikey`: the current apikey (if one)\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jsoup.HtmlPatcher }\n\n## Html Patcher\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"HtmlPatcher\" : {\n \"appendHead\" : [ ],\n \"appendBody\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.log4j.Log4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `Log4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them.\n\nThis plugin can accept the following configuration\n\n```javascript\n{\n \"Log4ShellFilter\": {\n \"status\": 200, // the status send back when an attack expression is found\n \"body\": \"\", // the body send back when an attack expression is found\n \"parseBody\": false // enables request body parsing to find attack expression\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"Log4ShellFilter\" : {\n \"status\" : 200,\n \"body\" : \"\",\n \"parseBody\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.loggers.BodyLogger }\n\n## Body logger\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BodyLogger`\n\n### Description\n\nThis plugin can log body present in request and response. It can just logs it, store in in the redis store with a ttl and send it to analytics.\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"BodyLogger\": {\n \"enabled\": true, // enabled logging\n \"log\": true, // just log it\n \"store\": false, // store bodies in datastore\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"sendToAnalytics\": false, // send bodies to analytics\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"password\": \"password\", // password for the ui, if none, it's public\n \"filter\": { // log only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"BodyLogger\" : {\n \"enabled\" : true,\n \"log\" : true,\n \"store\" : false,\n \"ttl\" : 300000,\n \"sendToAnalytics\" : false,\n \"maxSize\" : 5242880,\n \"password\" : \"password\",\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.mirror.MirroringPlugin }\n\n## Mirroring plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `MirroringPlugin`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"MirroringPlugin\": {\n \"enabled\": true, // enabled mirroring\n \"to\": \"https://foo.bar.dev\", // the url of the service to mirror\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"MirroringPlugin\" : {\n \"enabled\" : true,\n \"to\" : \"https://foo.bar.dev\",\n \"captureResponse\" : false,\n \"generateEvents\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oauth1.OAuth1CallerPlugin }\n\n## OAuth1 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"OAuth1Caller\" : {\n \"algo\" : \"HmacSHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oidc.OIDCHeaders }\n\n## OIDC headers\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCHeaders\" : {\n \"profile\" : {\n \"send\" : true,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idtoken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accesstoken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.security.SecurityTxt }\n\n## Security Txt\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `SecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"SecurityTxt\": {\n \"Contact\": \"contact@foo.bar\", // mandatory, a link or e-mail address for people to contact you about security issues\n \"Encryption\": \"http://url-to-public-key\", // optional, a link to a key which security researchers should use to securely talk to you\n \"Acknowledgments\": \"http://url\", // optional, a link to a web page where you say thank you to security researchers who have helped you\n \"Preferred-Languages\": \"en, fr, es\", // optional\n \"Policy\": \"http://url\", // optional, a link to a policy detailing what security researchers should do when searching for or reporting security issues\n \"Hiring\": \"http://url\", // optional, a link to any security-related job openings in your organisation\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"SecurityTxt\" : {\n \"Contact\" : \"contact@foo.bar\",\n \"Encryption\" : \"https://...\",\n \"Acknowledgments\" : \"https://...\",\n \"Preferred-Languages\" : \"en, fr\",\n \"Policy\" : \"https://...\",\n \"Hiring\" : \"https://...\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.static.StaticResponse }\n\n## Static Response\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `StaticResponse`\n\n### Description\n\nThis plugin returns a static response for any request\n\n\n\n### Default configuration\n\n```json\n{\n \"StaticResponse\" : {\n \"status\" : 200,\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n },\n \"body\" : \"{\\\"message\\\":\\\"hello world!\\\"}\",\n \"bodyBase64\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/user-agent`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoHeader }\n\n## User-Agent header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `UserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfoHeader\": {\n \"headerName\": \"X-User-Agent-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfoHeader\" : {\n \"headerName\" : \"X-User-Agent-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.workflow.WorkflowEndpoint }\n\n## [DEPRECATED] Workflow endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `WorkflowEndpoint`\n\n### Description\n\nThis plugin runs a workflow and return the response\n\n\n\n### Default configuration\n\n```json\n{\n \"WorkflowEndpoint\" : {\n \"workflow\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.biscuit.BiscuitValidator }\n\n## Biscuit token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nThis plugin validates a Biscuit token.\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nexpected response from http service is\n\n```json\n{\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n}\n```\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"url\": \"...\", // url for the call\n \"headers\": {}, // http header for the call\n \"ttl\": 600000, // cache ttl,\n \"mtlsConfig\": {\n \"certId\": \"xxxxx\",\n \"mtls\": false,\n \"loose\": false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingHttpValidator\" : {\n \"url\" : \"http://foo.bar\",\n \"ttl\" : 600000,\n \"headers\" : { },\n \"mtlsConfig\" : {\n \"certId\" : \"...\",\n \"mtls\" : false,\n \"loose\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingValidator }\n\n## Client certificate matching\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\" : {\n \"serialNumbers\" : [ ],\n \"subjectDNs\" : [ ],\n \"issuerDNs\" : [ ],\n \"regexSubjectDNs\" : [ ],\n \"regexIssuerDNs\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertValidator }\n\n## Client Certificate Only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.hmac.HMACValidator }\n\n## HMAC access validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HMACAccessValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACAccessValidator\" : {\n \"secret\" : \"\"\n }\n}\n```\n\n\n\n### Documentation\n\n\n The HMAC signature needs to be set on the `Authorization` or `Proxy-Authorization` header.\n The format of this header should be : `hmac algorithm=\"\", headers=\"
\", signature=\"\"`\n As example, a simple nodeJS call with the expected header\n ```js\n const crypto = require('crypto');\n const fetch = require('node-fetch');\n\n const date = new Date()\n const secret = \"my-secret\" // equal to the api key secret by default\n\n const algo = \"sha512\"\n const signature = crypto.createHmac(algo, secret)\n .update(date.getTime().toString())\n .digest('base64');\n\n fetch('http://myservice.oto.tools:9999/api/test', {\n headers: {\n \"Otoroshi-Client-Id\": \"my-id\",\n \"Otoroshi-Client-Secret\": \"my-secret\",\n \"Date\": date.getTime().toString(),\n \"Authorization\": `hmac algorithm=\"hmac-${algo}\", headers=\"Date\", signature=\"${signature}\"`,\n \"Accept\": \"application/json\"\n }\n })\n .then(r => r.json())\n .then(console.log)\n ```\n In this example, we have an Otoroshi service deployed on http://myservice.oto.tools:9999/api/test, protected by api keys.\n The secret used is the secret of the api key (by default, but you can change it and define a secret on the plugin configuration).\n We send the base64 encoded date of the day, signed by the secret, in the Authorization header. We specify the headers signed and the type of algorithm used.\n You can sign more than one header but you have to list them in the headers fields (each one separate by a space, example : headers=\"Date KeyId\").\n The algorithm used can be HMAC-SHA1, HMAC-SHA256, HMAC-SHA384 or HMAC-SHA512.\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.oidc.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.quotas.ServiceQuotas }\n\n## Public quotas\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `ServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current service\n\n\n\n\n\n\n\n### Default configuration\n\n```json\n{\n \"ServiceQuotas\" : {\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.users.HasAllowedUsersValidator }\n\n## Allowed users only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasAllowedUsersValidator\": {\n \"usernames\": [], // allowed usernames\n \"emails\": [], // allowed user email addresses\n \"emailDomains\": [], // allowed user email domains\n \"metadataMatch\": [], // json path expressions to match against user metadata. passes if one match\n \"metadataNotMatch\": [], // json path expressions to match against user metadata. passes if none match\n \"profileMatch\": [], // json path expressions to match against user profile. passes if one match\n \"profileNotMatch\": [], // json path expressions to match against user profile. passes if none match\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasAllowedUsersValidator\" : {\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"emailDomains\" : [ ],\n \"metadataMatch\" : [ ],\n \"metadataNotMatch\" : [ ],\n \"profileMatch\" : [ ],\n \"profileNotMatch\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ApikeyAuthModule }\n\n## Apikey auth module\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"ApikeyAuthModule\" : {\n \"realm\" : \"apikey-auth-module-realm\",\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : [ ],\n \"oneMetaIn\" : [ ],\n \"allMetaIn\" : [ ],\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.CertificateAsApikey }\n\n## Client certificate as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `CertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"CertificateAsApikey\" : {\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ClientCredentialFlowExtractor }\n\n## Client Credential Flow ApiKey extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin can extract an apikey from an opaque access_token generate by the `ClientCredentialFlow` plugin\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.biscuit.BiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.discovery.DiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.IpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"apikey\": \"xxxxxxx\",\n \"timeout\": 2000, // timeout in ms\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"apikey\" : \"xxxxxxx\",\n \"timeout\" : 2000,\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.MaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"path\": \"/foo/bar/cities.mmdb\", // file path, can be \"global\"\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"path\" : \"global\",\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.jwt.JwtUserExtractor }\n\n## Jwt user extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `JwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"JwtUserExtractor\" : {\n \"verifier\" : \"\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\" : \"email\",\n \"metaPath\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.oidc.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenAsApikey\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.useragent.UserAgentExtractor }\n\n## User-Agent details extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `UserAgentInfo`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfo\": {\n \"log\": false // will log user-agent details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfo\" : {\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.apikeys.ClientCredentialService }\n\n## Client Credential Service\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `ClientCredentialService`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.discovery.DiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator }\n\n## Kubernetes admission validator webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to handle manifests validation\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector }\n\n## Kubernetes sidecar injector webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to inject otoroshi-sidecar in pods\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.jobs.StateExporter }\n\n## Otoroshi state exporter job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `StateExporter`\n\n### Description\n\nThis job send an event containing the full otoroshi export every n seconds\n\n\n\n### Default configuration\n\n```json\n{\n \"StateExporter\" : {\n \"every_sec\" : 3600,\n \"format\" : \"json\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleCertificatesFetcherJob }\n\n## Tailscale certificate fetcher job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will fetch certificates from Tailscale ACME provider\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleTargetsJob }\n\n## Tailscale targets job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will aggregates Tailscale possible online targets\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesIngressControllerJob }\n\n## Kubernetes Ingress Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi as an Ingress Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesOtoroshiCRDsControllerJob }\n\n## Kubernetes Otoroshi CRDs Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi CRDs Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesToOtoroshiCertSyncJob }\n\n## Kubernetes to Otoroshi certs. synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. TLS secrets from Kubernetes to Otoroshi\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.OtoroshiToKubernetesCertSyncJob }\n\n## Otoroshi certs. to Kubernetes secrets synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. Otoroshi certs to Kubernetes TLS secrets\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.wasm.WasmVmPoolCleaner }\n\n## otoroshi.wasm.WasmVmPoolCleaner\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.next.proxy.ProxyEngine }\n\n## Otoroshi next proxy engine (experimental)\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `NextGenProxyEngine`\n\n### Description\n\nThis plugin holds the next generation otoroshi proxy engine implementation. This engine is **experimental** and may not work as expected !\n\nYou can active this plugin only on some domain names so you can easily A/B test the new engine.\nThe new proxy engine is designed to be more reactive and more efficient generally.\nIt is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nThe idea is to only rely on plugins to work and avoid losing time with features that are not used in service descriptors.\nAn automated conversion happens for every service descriptor. If the exposed domain is handled by this plugin, it will be served by this plugin.\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `route-compositions`: multiple routing rules based on hostname, path, method and headers that will execute the same list of plugins\n - `backends`: a list of targets to contact a backend\n\nas an example, let say you want to use the new engine on your service exposed on `api.foo.bar/api`.\nTo do that, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration,\nenabled it and in `domains` add the value `api.foo.bar` (it is possible to use `*.foo.bar` if that's what you want to do).\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the old one.\n\n\n\n### Default configuration\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"domains\" : [ \"*\" ],\n \"deny_domains\" : [ ],\n \"reporting\" : true,\n \"merge_sync_steps\" : true,\n \"export_reporting\" : false,\n \"apply_legacy_checks\" : true,\n \"debug\" : false,\n \"capture\" : false,\n \"captureMaxEntitySize\" : 4194304,\n \"debug_headers\" : false,\n \"routing_strategy\" : \"tree\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.script.ForwardTrafficHandler }\n\n## Forward traffic\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `ForwardTrafficHandler`\n\n### Description\n\nThis plugin can be use to perform a raw traffic forward to an URL without passing through otoroshi routing\n\n\n\n### Default configuration\n\n```json\n{\n \"ForwardTrafficHandler\" : {\n \"domains\" : {\n \"my.domain.tld\" : {\n \"baseUrl\" : \"https://my.otherdomain.tld\",\n \"secret\" : \"jwt signing secret\",\n \"service\" : {\n \"id\" : \"service id for analytics\",\n \"name\" : \"service name for analytics\"\n }\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n\n\n"},{"name":"built-in-plugins.md","id":"/plugins/built-in-plugins.md","url":"/plugins/built-in-plugins.html","title":"Built-in plugins","content":"# Built-in plugins\n\nOtoroshi next provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration.\n\n
\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersIn }\n\n## Additional headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersIn`\n\n### Description\n\nThis plugin adds headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersOut }\n\n## Additional headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersOut`\n\n### Description\n\nThis plugin adds headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AllowHttpMethods }\n\n## Allowed HTTP methods\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AllowHttpMethods`\n\n### Description\n\nThis plugin verifies the current request only uses allowed http methods\n\n\n\n### Default configuration\n\n```json\n{\n \"allowed\" : [ ],\n \"forbidden\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyAuthModule }\n\n## Apikey auth module\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"realm\" : \"apikey-auth-module-realm\",\n \"matcher\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyCalls }\n\n## Apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyCalls`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass\n\n\n\n### Default configuration\n\n```json\n{\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyQuotas }\n\n## Apikey quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyQuotas`\n\n### Description\n\nIncrements quotas for the currents apikey. Useful when 'legacy checks' are disabled on a service/globally or when apikey are extracted in a custom fashion.\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AuthModule }\n\n## Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AuthModule`\n\n### Description\n\nThis plugin applies an authentication module\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BasicAuthCaller }\n\n## Basic Auth. caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : null,\n \"passaword\" : null,\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BrotliResponseCompressor }\n\n## Brotli compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BrotliResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using brotli\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BuildMode }\n\n## Build mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BuildMode`\n\n### Description\n\nThis plugin displays a build page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.CanaryMode }\n\n## Canary mode\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.CanaryMode`\n\n### Description\n\nThis plugin can split a portion of the traffic to canary backends\n\n\n\n### Default configuration\n\n```json\n{\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ContextValidation }\n\n## Context validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ContextValidation`\n\n### Description\n\nThis plugin validates the current context using JSONPath validators.\n\nThis plugin let you configure a list of validators that will check if the current call can pass.\nA validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value.\nThe JSONPath will be applied on a document that will look like\n\n```js\n{\n \"snowflake\" : \"1516772930422308903\",\n \"apikey\" : { // current apikey\n \"clientId\" : \"vrmElDerycXrofar\",\n \"clientName\" : \"default-apikey\",\n \"metadata\" : {\n \"foo\" : \"bar\"\n },\n \"tags\" : [ ]\n },\n \"user\" : null, // current user\n \"request\" : {\n \"id\" : 1,\n \"method\" : \"GET\",\n \"headers\" : {\n \"Host\" : \"ctx-validation-next-gen.oto.tools:9999\",\n \"Accept\" : \"*/*\",\n \"User-Agent\" : \"curl/7.64.1\",\n \"Authorization\" : \"Basic dnJtRWxEZXJ5Y1hyb2ZhcjpvdDdOSTkyVGI2Q2J4bWVMYU9UNzJxamdCU2JlRHNLbkxtY1FBcXBjVjZTejh0Z3I1b2RUOHAzYjB5SEVNRzhZ\",\n \"Remote-Address\" : \"127.0.0.1:58929\",\n \"Timeout-Access\" : \"\",\n \"Raw-Request-URI\" : \"/foo\",\n \"Tls-Session-Info\" : \"Session(1650461821330|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\" : [ ],\n \"tls\" : false,\n \"uri\" : \"/foo\",\n \"path\" : \"/foo\",\n \"version\" : \"HTTP/1.1\",\n \"has_body\" : false,\n \"remote\" : \"127.0.0.1\",\n \"client_cert_chain\" : null\n },\n \"config\" : {\n \"validators\" : [ {\n \"path\" : \"$.apikey.metadata.foo\",\n \"value\" : \"bar\"\n } ]\n },\n \"global_config\" : { ... }, // global config\n \"attrs\" : {\n \"otoroshi.core.SnowFlake\" : \"1516772930422308903\",\n \"otoroshi.core.ElCtx\" : {\n \"requestId\" : \"1516772930422308903\",\n \"requestSnowflake\" : \"1516772930422308903\",\n \"requestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\"\n },\n \"otoroshi.next.core.Report\" : \"otoroshi.next.proxy.NgExecutionReport@277b44e2\",\n \"otoroshi.core.RequestStart\" : 1650461821545,\n \"otoroshi.core.RequestWebsocket\" : false,\n \"otoroshi.core.RequestCounterOut\" : 0,\n \"otoroshi.core.RemainingQuotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 0,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 2,\n \"remainingCallsPerDay\" : 9999998,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 269,\n \"remainingCallsPerMonth\" : 9999731\n },\n \"otoroshi.next.core.MatchedRoutes\" : \"MutableList(route_022825450-e97d-42ed-8e22-b23342c1c7c8)\",\n \"otoroshi.core.RequestNumber\" : 1,\n \"otoroshi.next.core.Route\" : { ... }, // current route as json\n \"otoroshi.core.RequestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\",\n \"otoroshi.core.ApiKey\" : { ... }, // current apikey as json\n \"otoroshi.core.User\" : { ... }, // current user as json\n \"otoroshi.core.RequestCounterIn\" : 0\n },\n \"route\" : { ... },\n \"token\" : null // current valid jwt token if one\n}\n```\n\nthe expected value support some syntax tricks like\n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current apikey has a metadata name `foo` with a value containing `bar`, you can write the following validator\n\n```js\n{\n \"path\": \"$.apikey.metadata.foo\",\n \"value\": \"Contains(bar)\"\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"validators\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Cors }\n\n## CORS\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Cors`\n\n### Description\n\nThis plugin applies CORS rules\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_origin\" : \"*\",\n \"expose_headers\" : [ ],\n \"allow_headers\" : [ ],\n \"allow_methods\" : [ ],\n \"excluded_patterns\" : [ ],\n \"max_age\" : null,\n \"allow_credentials\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.DisableHttp10 }\n\n## Disable HTTP/1.0\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.DisableHttp10`\n\n### Description\n\nThis plugin forbids HTTP/1.0 requests\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EndlessHttpResponse }\n\n## Endless HTTP responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EndlessHttpResponse`\n\n### Description\n\nThis plugin returns 128 Gb of 0 to the ip addresses is in the list\n\n\n\n### Default configuration\n\n```json\n{\n \"finger\" : false,\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaServerSink }\n\n## Eureka instance\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaServerSink`\n\n### Description\n\nEureka plugin description\n\n\n\n### Default configuration\n\n```json\n{\n \"evictionTimeout\" : 300\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaTarget }\n\n## Internal Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an internal Eureka server.\n If you want to use a target which it locate outside of Otoroshi, you must use the External Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ExternalEurekaTarget }\n\n## External Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ExternalEurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an external Eureka server.\n If you want to use a target that is directly exposed by an implementation of Eureka by Otoroshi,\n you must use the Internal Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForceHttpsTraffic }\n\n## Force HTTPS traffic\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForceHttpsTraffic`\n\n### Description\n\nThis plugin verifies the current request uses HTTPS\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForwardedHeader }\n\n## Forwarded header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForwardedHeader`\n\n### Description\n\nThis plugin adds all the Forwarded header to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalMaintenanceMode }\n\n## Global Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalMaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page for every services. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalPerIpAddressThrottling }\n\n## Global per ip address throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalPerIpAddressThrottling`\n\n### Description\n\nEnforce global per ip address throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalThrottling }\n\n## Global throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalThrottling`\n\n### Description\n\nEnforce global throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLBackend }\n\n## GraphQL Composer\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLBackend`\n\n### Description\n\nThis plugin exposes a GraphQL API that you can compose with whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"schema\" : \"\\n type User {\\n name: String!\\n firstname: String!\\n }\\n\\n type Query {\\n users: [User] @json(data: \\\"[{ \\\\\\\"firstname\\\\\\\": \\\\\\\"Foo\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Bar\\\\\\\" }, { \\\\\\\"firstname\\\\\\\": \\\\\\\"Bar\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Foo\\\\\\\" }]\\\")\\n }\\n \",\n \"permissions\" : [ ],\n \"initial_data\" : null,\n \"max_depth\" : 15\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLProxy }\n\n## GraphQL Proxy\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLProxy`\n\n### Description\n\nThis plugin can apply validations (query, schema, max depth, max complexity) on graphql endpoints\n\n\n\n### Default configuration\n\n```json\n{\n \"endpoint\" : \"https://countries.trevorblades.com/graphql\",\n \"schema\" : null,\n \"max_depth\" : 50,\n \"max_complexity\" : 50000,\n \"path\" : \"/graphql\",\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLQuery }\n\n## GraphQL Query to REST\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLQuery`\n\n### Description\n\nThis plugin can be used to call GraphQL query endpoints and expose it as a REST endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://some.graphql/endpoint\",\n \"headers\" : { },\n \"method\" : \"POST\",\n \"query\" : \"{\\n\\n}\",\n \"timeout\" : 60000,\n \"response_path\" : null,\n \"response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GzipResponseCompressor }\n\n## Gzip compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GzipResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using gzip\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACCaller }\n\n## HMAC caller plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACCaller`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null,\n \"algo\" : \"HMAC-SHA512\",\n \"authorizationHeader\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACValidator }\n\n## HMAC access validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HeadersValidation }\n\n## Headers validation\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HeadersValidation`\n\n### Description\n\nThis plugin validates the values of incoming request headers\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Http3Switch }\n\n## Http3 traffic switch\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Http3Switch`\n\n### Description\n\nThis plugin injects additional alt-svc header to switch to the http3 server\n\n\n\n### Default configuration\n\n```json\n{\n \"ma\" : 3600\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ImageReplacer }\n\n## Image replacer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ImageReplacer`\n\n### Description\n\nReplace all response with content-type image/* as they are proxied\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://raw.githubusercontent.com/MAIF/otoroshi/master/resources/otoroshi-logo.png\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressAllowedList }\n\n## IP allowed list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressAllowedList`\n\n### Description\n\nThis plugin verifies the current request ip address is in the allowed list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressBlockList }\n\n## IP block list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressBlockList`\n\n### Description\n\nThis plugin verifies the current request ip address is not in the blocked list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQ }\n\n## JQ\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQ`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"request\" : \".\",\n \"response\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQRequest }\n\n## JQ transform request\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQRequest`\n\n### Description\n\nThis plugin let you transform request JSON body using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQResponse }\n\n## JQ transform response\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQResponse`\n\n### Description\n\nThis plugin let you transform JSON response using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlRequest }\n\n## request body json-to-xml\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlRequest`\n\n### Description\n\nThis plugin transform incoming request body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlResponse }\n\n## response body json-to-xml\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlResponse`\n\n### Description\n\nThis plugin transform response body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtSigner }\n\n## Jwt signer\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtSigner`\n\n### Description\n\nThis plugin can only generate token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"replace_if_present\" : true,\n \"fail_if_present\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerification }\n\n## Jwt verifiers\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerification`\n\n### Description\n\nThis plugin verifies the current request with one or more jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifiers\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerificationOnly }\n\n## Jwt verification only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerificationOnly`\n\n### Description\n\nThis plugin verifies the current request with one jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"fail_if_absent\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MaintenanceMode }\n\n## Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersIn }\n\n## Missing headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersIn`\n\n### Description\n\nThis plugin adds headers (if missing) in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersOut }\n\n## Missing headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersOut`\n\n### Description\n\nThis plugin adds headers (if missing) in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MockResponses }\n\n## Mock Responses\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MockResponses`\n\n### Description\n\nThis plugin returns mock responses\n\n\n\n### Default configuration\n\n```json\n{\n \"responses\" : [ ],\n \"pass_through\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MultiAuthModule }\n\n## Multi Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MultiAuthModule`\n\n### Description\n\nThis plugin applies an authentication module from a list of selected modules\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_modules\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleExpectedUser }\n\n## User logged in expected\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleExpectedUser`\n\n### Description\n\nThis plugin enforce that a user from any auth. module is logged in\n\n\n\n### Default configuration\n\n```json\n{\n \"only_from\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleUserExtractor }\n\n## User extraction from auth. module\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleUserExtractor`\n\n### Description\n\nThis plugin extracts users from an authentication module without enforcing login\n\n\n\n### Default configuration\n\n```json\n{\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitExtractor`\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitValidator }\n\n## Biscuit token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitValidator`\n\n### Description\n\nThis plugin validates a Biscuit token\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCertificateAsApikey }\n\n## Client certificate as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"read_only\" : false,\n \"allow_client_id_only\" : false,\n \"throttling_quota\" : 100,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000,\n \"constrained_services_only\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCertChainHeader }\n\n## Client certificate header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCertChainHeader`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers\n\n\n\n### Default configuration\n\n```json\n{\n \"send_pem\" : false,\n \"pem_header_name\" : \"X-Client-Cert-Pem\",\n \"send_dns\" : false,\n \"dns_header_name\" : \"X-Client-Cert-DNs\",\n \"send_chain\" : false,\n \"chain_header_name\" : \"X-Client-Cert-Chain\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentialTokenEndpoint }\n\n## Client credential token endpoint\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentialTokenEndpoint`\n\n### Description\n\nThis plugin provide the endpoint for the client_credential flow token endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentials }\n\n## Client Credential Service\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentials`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\",\n \"domain\" : \"*\",\n \"secure\" : true,\n \"biscuit\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomQuotas }\n\n## Custom quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomQuotas`\n\n### Description\n\nThis plugin will enforce quotas on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomThrottling }\n\n## Custom throttling\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomThrottling`\n\n### Description\n\nThis plugin will enforce throttling on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"throttling_quota\" : 100\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDefaultRequestBody }\n\n## Default request body\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDefaultRequestBody`\n\n### Description\n\nThis plugin adds a default request body if none specified\n\n\n\n### Default configuration\n\n```json\n{\n \"bodyBinary\" : \"\",\n \"contentType\" : \"text/plain\",\n \"contentEncoding\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDeferPlugin }\n\n## Defer Responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\n\n\n### Default configuration\n\n```json\n{\n \"duration\" : 0\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationSink`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoveryTargetsSelector`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgErrorRewriter }\n\n## Error response rewrite\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgErrorRewriter`\n\n### Description\n\nThis plugin catch http response with specific statuses and rewrite the response\n\n\n\n### Default configuration\n\n```json\n{\n \"ranges\" : [ {\n \"from\" : 500,\n \"to\" : 599\n } ],\n \"templates\" : {\n \"default\" : \"\\n \\n

An error occurred with id: ${error_id}

\\n

please contact your administrator with this error id !

\\n \\n\"\n },\n \"log\" : true,\n \"export\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoEndpoint`\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint `/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoHeader }\n\n## Geolocation header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasAllowedUsersValidator }\n\n## Allowed users only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\n\n\n### Default configuration\n\n```json\n{\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"email_domains\" : [ ],\n \"metadata_match\" : [ ],\n \"metadata_not_match\" : [ ],\n \"profile_match\" : [ ],\n \"profile_not_match\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator`\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following fetched from an http endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingValidator }\n\n## Client certificate matching\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertValidator }\n\n## Client Certificate Only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertValidator`\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHtmlPatcher }\n\n## Html Patcher\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"append_head\" : [ ],\n \"append_body\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHttpClientCache }\n\n## HTTP Client Cache\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHttpClientCache`\n\n### Description\n\nThis plugin add cache headers to responses\n\n\n\n### Default configuration\n\n```json\n{\n \"max_age_seconds\" : 86400,\n \"methods\" : [ \"GET\" ],\n \"status\" : [ 200 ],\n \"mime_types\" : [ \"text/html\" ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"apikey\" : null,\n \"timeout\" : 2000,\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Canary }\n\n## Izanami V1 Canary Campaign\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Canary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test)\n\n\n\n### Default configuration\n\n```json\n{\n \"experiment_id\" : \"foo:bar:qix\",\n \"config_id\" : \"foo:bar:qix:config\",\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 5000,\n \"route_config\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Proxy }\n\n## Izanami v1 APIs Proxy\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Proxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"/api/izanami\",\n \"feature_pattern\" : \"*\",\n \"config_pattern\" : \"*\",\n \"auto_context\" : false,\n \"features_enabled\" : true,\n \"features_with_context_enabled\" : true,\n \"configuration_enabled\" : false,\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 500\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgJwtUserExtractor }\n\n## Jwt user extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgJwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : \"none\",\n \"strict\" : true,\n \"strip\" : false,\n \"name_path\" : null,\n \"email_path\" : null,\n \"meta_path\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyApikeyCall }\n\n## Legacy apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyApikeyCall`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass. This plugin behaves exactly like the service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyAuthModuleCall }\n\n## Legacy Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyAuthModuleCall`\n\n### Description\n\nThis plugin applies an authentication module the same way service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLog4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLog4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"body\" : \"\",\n \"parse_body\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"global\",\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgResponseCache }\n\n## Response Cache\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\n\n\n### Default configuration\n\n```json\n{\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgSecurityTxt }\n\n## Security Txt\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgSecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/)\n\n\n\n### Default configuration\n\n```json\n{\n \"contact\" : \"contact@foo.bar\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgServiceQuotas }\n\n## Public quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current route\n\n\n\n### Default configuration\n\n```json\n{\n \"throttling_quota\" : 10000000,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgTrafficMirroring }\n\n## Traffic Mirroring\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgTrafficMirroring`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\n\n\n### Default configuration\n\n```json\n{\n \"to\" : \"https://foo.bar.dev\",\n \"enabled\" : true,\n \"capture_response\" : false,\n \"generate_events\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentExtractor }\n\n## User-Agent details extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentExtractor`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoEndpoint`\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint: /.well-known/otoroshi/plugins/user-agent\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoHeader }\n\n## User-Agent header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth1Caller }\n\n## OAuth1 caller\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"consumerKey\" : null,\n \"consumerSecret\" : null,\n \"token\" : null,\n \"tokenSecret\" : null,\n \"algo\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth2Caller }\n\n## OAuth2 caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"client_credentials\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : null,\n \"audience\" : null,\n \"user\" : null,\n \"password\" : null,\n \"cacheTokenSeconds\" : 600000,\n \"tlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCHeaders }\n\n## OIDC headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"profile\" : {\n \"send\" : false,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idToken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accessToken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiChallenge }\n\n## Otoroshi challenge token\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiChallenge`\n\n### Description\n\nThis plugin adds a jwt challenge token to the request to a backend and expects a response with a matching token\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"V2\",\n \"ttl\" : 30,\n \"request_header_name\" : null,\n \"response_header_name\" : null,\n \"algo_to_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"algo_from_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"state_resp_leeway\" : 10\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiHeadersIn }\n\n## Otoroshi headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiHeadersIn`\n\n### Description\n\nThis plugin adds Otoroshi specific headers to the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiInfos }\n\n## Otoroshi info. token\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiInfos`\n\n### Description\n\nThis plugin adds a jwt token with informations about the caller to the backend\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"Latest\",\n \"ttl\" : 30,\n \"header_name\" : null,\n \"add_fields\" : null,\n \"algo\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OverrideHost }\n\n## Override host header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OverrideHost`\n\n### Description\n\nThis plugin override the current Host header with the Host of the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.PublicPrivatePaths }\n\n## Public/Private paths\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.PublicPrivatePaths`\n\n### Description\n\nThis plugin allows or forbid request based on path patterns\n\n\n\n### Default configuration\n\n```json\n{\n \"strict\" : false,\n \"private_patterns\" : [ ],\n \"public_patterns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.QueryTransformer }\n\n## Query param transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.QueryTransformer`\n\n### Description\n\nThis plugin can modify the query params of the request\n\n\n\n### Default configuration\n\n```json\n{\n \"remove\" : [ ],\n \"rename\" : { },\n \"add\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RBAC }\n\n## RBAC\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RBAC`\n\n### Description\n\nThis plugin check if current user/apikey/jwt token has the right role\n\n\n\n### Default configuration\n\n```json\n{\n \"allow\" : [ ],\n \"deny\" : [ ],\n \"allow_all\" : false,\n \"deny_all\" : false,\n \"jwt_path\" : null,\n \"apikey_path\" : null,\n \"user_path\" : null,\n \"role_prefix\" : null,\n \"roles\" : \"roles\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ReadOnlyCalls }\n\n## Read only requests\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ReadOnlyCalls`\n\n### Description\n\nThis plugin verifies the current request only reads data\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Redirection }\n\n## Redirection\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Redirection`\n\n### Description\n\nThis plugin redirects the current request elsewhere\n\n\n\n### Default configuration\n\n```json\n{\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersIn }\n\n## Remove headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersIn`\n\n### Description\n\nThis plugin removes headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersOut }\n\n## Remove headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersOut`\n\n### Description\n\nThis plugin removes headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Robots }\n\n## Robots\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Robots`\n\n### Description\n\nThis plugin provides all the necessary tool to handle search engine robots\n\n\n\n### Default configuration\n\n```json\n{\n \"robot_txt_enabled\" : true,\n \"robot_txt_content\" : \"User-agent: *\\nDisallow: /\\n\",\n \"meta_enabled\" : true,\n \"meta_content\" : \"noindex,nofollow,noarchive\",\n \"header_enabled\" : true,\n \"header_content\" : \"noindex, nofollow, noarchive\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RoutingRestrictions }\n\n## Routing Restrictions\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RoutingRestrictions`\n\n### Description\n\nThis plugin apply routing restriction `method domain/path` on the current request/route\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_last\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"not_found\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.S3Backend }\n\n## S3 Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.S3Backend`\n\n### Description\n\nThis plugin is able to S3 bucket with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"bucket\" : \"\",\n \"endpoint\" : \"\",\n \"region\" : \"eu-west-1\",\n \"access\" : \"client\",\n \"secret\" : \"secret\",\n \"key\" : \"\",\n \"chunkSize\" : 8388608,\n \"v4auth\" : true,\n \"writeEvery\" : 60000,\n \"acl\" : \"private\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SOAPAction }\n\n## SOAP action\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SOAPAction`\n\n### Description\n\nThis plugin is able to call SOAP actions and expose it as a rest endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : null,\n \"envelope\" : \"\",\n \"action\" : null,\n \"preserve_query\" : true,\n \"charset\" : null,\n \"jq_request_filter\" : null,\n \"jq_response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SendOtoroshiHeadersBack }\n\n## Send otoroshi headers back\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SendOtoroshiHeadersBack`\n\n### Description\n\nThis plugin adds response header containing useful informations about the current call\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SnowMonkeyChaos }\n\n## Snow Monkey Chaos\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SnowMonkeyChaos`\n\n### Description\n\nThis plugin introduce some chaos into you life\n\n\n\n### Default configuration\n\n```json\n{\n \"large_request_fault\" : null,\n \"large_response_fault\" : null,\n \"latency_injection_fault\" : null,\n \"bad_responses_fault\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticBackend }\n\n## Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticBackend`\n\n### Description\n\nThis plugin is able to serve a static folder with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"root_path\" : \"/tmp\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticResponse }\n\n## Static Response\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticResponse`\n\n### Description\n\nThis plugin returns static responses\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"headers\" : { },\n \"body\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TailscaleSelectTargetByName }\n\n## Tailscale select target by name\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TailscaleSelectTargetByName`\n\n### Description\n\nThis plugin selects a machine instance on Tailscale network based on its name\n\n\n\n### Default configuration\n\n```json\n{\n \"machine_name\" : \"my-machine\",\n \"use_ip_address\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TcpTunnel }\n\n## TCP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TcpTunnel`\n\n### Description\n\nThis plugin creates TCP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.UdpTunnel }\n\n## UDP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.UdpTunnel`\n\n### Description\n\nThis plugin creates UDP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.W3CTracing }\n\n## W3C Trace Context\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.W3CTracing`\n\n### Description\n\nThis plugin propagates W3C Trace Context spans and can export it to Jaeger or Zipkin\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"noop\",\n \"endpoint\" : \"http://localhost:3333/spans\",\n \"timeout\" : 30000,\n \"baggage\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmAccessValidator }\n\n## Wasm Access control\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmAccessValidator`\n\n### Description\n\nDelegate route access to a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmBackend }\n\n## Wasm Backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmBackend`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as backend\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmOPA }\n\n## Open Policy Agent (OPA)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmOPA`\n\n### Description\n\nRepo policies as WASM modules\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : true,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmPreRoute }\n\n## Wasm pre-route\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmPreRoute`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as in pre-route phase\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRequestTransformer }\n\n## Wasm Request Transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRequestTransformer`\n\n### Description\n\nTransform the content of the request with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmResponseTransformer }\n\n## Wasm Response Transformer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmResponseTransformer`\n\n### Description\n\nTransform the content of a response with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouteMatcher }\n\n## Wasm Route Matcher\n\n### Defined on steps\n\n - `MatchRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouteMatcher`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as route matcher\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouter }\n\n## Wasm Router\n\n### Defined on steps\n\n - `Router`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouter`\n\n### Description\n\nCan decide for routing with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmSink }\n\n## Wasm Sink\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmSink`\n\n### Description\n\nHandle unmatched requests with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XForwardedHeaders }\n\n## X-Forwarded-* headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XForwardedHeaders`\n\n### Description\n\nThis plugin adds all the X-Forwarded-* headers to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonRequest }\n\n## request body xml-to-json\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonRequest`\n\n### Description\n\nThis plugin transform incoming request body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonResponse }\n\n## response body xml-to-json\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonResponse`\n\n### Description\n\nThis plugin transform response body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ZipFileBackend }\n\n## Zip file backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ZipFileBackend`\n\n### Description\n\nServes content from a zip file\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://github.com/MAIF/otoroshi/releases/download/16.11.2/otoroshi-manual-16.11.2.zip\",\n \"headers\" : { },\n \"dir\" : \"./zips\",\n \"prefix\" : null,\n \"ttl\" : 3600000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.tunnel.TunnelPlugin }\n\n## Remote tunnel calls\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.tunnel.TunnelPlugin`\n\n### Description\n\nThis plugin can contact remote service using tunnels\n\n\n\n### Default configuration\n\n```json\n{\n \"tunnel_id\" : \"default\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.wasm.proxywasm.NgCorazaWAF }\n\n## Coraza WAF\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.wasm.proxywasm.NgCorazaWAF`\n\n### Description\n\nCoraza WAF plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"ref\" : \"none\"\n}\n```\n\n\n\n\n\n@@@\n\n"},{"name":"create-plugins.md","id":"/plugins/create-plugins.md","url":"/plugins/create-plugins.html","title":"Create plugins","content":"# Create plugins\n\n@@@ warning\nThis section is under rewrite. The following content is deprecated\n@@@\n\nWhen everything has failed and you absolutely need a feature in Otoroshi to make your use case work, there is a solution. Plugins is the feature in Otoroshi that allow you to code how Otoroshi should behave when receiving, validating and routing an http request. With request plugin, you can change request / response headers and request / response body the way you want, provide your own apikey, etc.\n\n## Plugin types\n\nthere are many plugin types explained @ref:[here](./plugins.md) \n\n## Code and signatures\n\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/requestsink.scala#L14-L19\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/routing.scala#L75-L78\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/accessvalidator.scala#L65-L85\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/script.scala#269-L540\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/eventlistener.scala#L27-L48\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L69-L164\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L108-L110\n\n\nfor more information about APIs you can use\n\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#package\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.mvc.Results\n* https://github.com/MAIF/otoroshi\n* https://doc.akka.io/docs/akka/2.5/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/scaladsl/Source.html\n\n## Plugin examples\n\n@ref:[A lot of plugins](./built-in-plugins.md) comes with otoroshi, you can find them on [github](https://github.com/MAIF/otoroshi/tree/master/otoroshi/app/plugins)\n\n## Writing a plugin from Otoroshi UI\n\nLog into Otoroshi and go to `Settings (cog icon) / Plugins`. Here you can create multiple request transformer scripts and associate it with service descriptor later.\n\n@@@ div { .centered-img }\n\n@@@\n\nwhen you write for instance a transformer in the Otoroshi UI, do the following\n\n```scala\nimport akka.stream.Materializer\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass MyTransformer extends RequestTransformer {\n\n val logger = Logger(\"my-transformer\")\n\n // implements the methods you want\n}\n\n// WARN: do not forget this line to provide a working instance of your transformer to Otoroshi\nnew MyTransformer()\n```\n\nYou can use the compile button to check if the script compiles, or code the transformer in your IDE (see next point).\n\nThen go to a service descriptor, scroll to the bottom of the page, and select your transformer in the list\n\n@@@ div { .centered-img }\n\n@@@\n\n## Providing a transformer from Java classpath\n\nYou can write your own transformer using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"request-transformer-example\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1.x.x\"\n )\n```\n\n@@@ warning\nyou MUST provide plugins that lies in the `otoroshi_plugins` package or in a sub-package of `otoroshi_plugins`. If you do not, your plugin will not be found by otoroshi. for example\n\n```scala\npackage otoroshi_plugins.com.my.company.myplugin\n```\n\nalso you don't have to instantiate your plugin at the end of the file like in the Otoroshi UI\n@@@\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/transformer.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in your service descriptor, you can chose your transformer in the list. If you want to do it from the API, you have to defined the transformerRef using `cp:` prefix like \n\n```json\n{\n \"transformerRef\": \"cp:otoroshi_plugins.my.class.package.MyTransformer\"\n}\n```\n\n## Getting custom configuration from the Otoroshi config. file\n\nLet say you need to provide custom configuration values for a script, then you can customize a configuration file of Otoroshi\n\n```hocon\ninclude \"application.conf\"\n\notoroshi {\n scripts {\n enabled = true\n }\n}\n\nmy-transformer {\n env = \"prod\"\n maxRequestBodySize = 2048\n maxResponseBodySize = 2048\n}\n```\n\nthen start Otoroshi like\n\n```sh\njava -Dconfig.file=/path/to/custom.conf -jar otoroshi.jar\n```\n\nthen, in your transformer, you can write something like \n\n```scala\npackage otoroshi_plugins.com.example.otoroshi\n\nimport akka.stream.Materializer\nimport akka.stream.scaladsl._\nimport akka.util.ByteString\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass BodyLengthLimiter extends RequestTransformer {\n\n override def def transformResponseWithCtx(ctx: TransformerResponseContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxResponseBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n\n override def transformRequestWithCtx(ctx: TransformerRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxRequestBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n}\n```\n\n## Using a library that is not embedded in Otoroshi\n\nJust use the `classpath` option when running Otoroshi\n\n```sh\njava -cp \"/path/to/library.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nBe carefull as your library can conflict with other libraries used by Otoroshi and affect its stability\n\n## Enabling plugins\n\nplugins can be enabled per service from the service settings page or globally from the danger zone in the plugins section.\n\n## Full example\n\na full external plugin example can be found @link:[here](https://github.com/mathieuancelin/otoroshi-wasmer-plugin)\n"},{"name":"index.md","id":"/plugins/index.md","url":"/plugins/index.html","title":"Otoroshi plugins","content":"# Otoroshi plugins\n\nIn this sections, you will find informations about Otoroshi plugins system\n\n* @ref:[Plugins system](./plugins.md)\n* @ref:[Create plugins](./create-plugins.md)\n* @ref:[Built in plugins](./built-in-plugins.md)\n* @ref:[Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@ index\n\n* [Plugins system](./plugins.md)\n* [Create plugins](./create-plugins.md)\n* [Built in plugins](./built-in-plugins.md)\n* [Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@"},{"name":"plugins.md","id":"/plugins/plugins.md","url":"/plugins/plugins.html","title":"Otoroshi plugins system","content":"# Otoroshi plugins system\n\nOtoroshi includes several extension points that allows you to create your own plugins and support stuff not supported by default.\n\n## Kind of available plugins\n\n@@@ div { .plugin-kind }\n###Request Sink\n\nUsed when no services are matched in Otoroshi. Can reply with any content.\n@@@\n\n@@@ div { .plugin-kind }\n###Pre routing\n\nUsed to extract values (like custom apikeys) and provide them to other plugins or Otoroshi engine\n@@@\n\n@@@ div { .plugin-kind }\n###Access Validator\n\nUsed to validate if a request can pass or not based on whatever you want\n@@@\n\n@@@ div { .plugin-kind }\n###Request Transformer\n\nUsed to transform request, responses and their body. Can be used to return arbitrary content\n@@@\n\n@@@ div { .plugin-kind }\n###Event listener\n\nAny plugin type can listen to Otoroshi internal events and react to thems\n@@@\n\n@@@ div { .plugin-kind }\n###Job\n\nTasks that can run automatically once, on be scheduled with a cron expression or every defined interval\n@@@\n\n@@@ div { .plugin-kind }\n###Exporter\n\nUsed to export events and Otoroshi alerts to an external source\n@@@\n\n@@@ div { .plugin-kind }\n###Request handler\n\nUsed to handle traffic without passing through Otoroshi routing and apply own rules\n@@@\n\n@@@ div { .plugin-kind }\n###Nano app\n\nUsed to write an api directly in Otoroshi in Scala language\n@@@"},{"name":"search.md","id":"/search.md","url":"/search.html","title":"Search otoroshi documentation","content":"# Search otoroshi documentation\n\n
\n\n"},{"name":"anonymous-reporting.md","id":"/topics/anonymous-reporting.md","url":"/topics/anonymous-reporting.html","title":"Anonymous reporting","content":"# Anonymous reporting\n\nThe best way of supporting us in Otoroshi developement is to enable Anonymous reporting.\n\n## Details\n\nWhen this feature is active, Otoroshi perdiodically send anonymous information about its configuration.\n\nThis information helps us to know how Otoroshi is used, it's a precious hint to prioritise our roadmap.\n\nBelow is an example of what is send by Otoroshi. You can find more information about these fields either on @ref:[entities documentation](../entities/index.md) or [by reading the source code](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/jobs/reporting.scala#L174-L458).\n\n```json\n{\n \"@timestamp\": 1679514537259,\n \"timestamp_str\": \"2023-03-22T20:48:57.259+01:00\",\n \"@id\": \"4edb54171-8156-4947-b821-41d6c2bd1ba7\",\n \"otoroshi_cluster_id\": \"1148aee35-a487-47b0-b494-a2a44862c618\",\n \"otoroshi_version\": \"16.0.0-dev\",\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"datastore\": \"file\",\n \"env\": \"dev\",\n \"features\": {\n \"snow_monkey\": false,\n \"clever_cloud\": false,\n \"kubernetes\": false,\n \"elastic_read\": true,\n \"lets_encrypt\": false,\n \"auto_certs\": false,\n \"wasm_manager\": true,\n \"backoffice_login\": false\n },\n \"stats\": {\n \"calls\": 3823,\n \"data_in\": 480406,\n \"data_out\": 4698261,\n \"rate\": 0,\n \"duration\": 35.89899494949495,\n \"overhead\": 24.696984848484846,\n \"data_in_rate\": 0,\n \"data_out_rate\": 0,\n \"concurrent_requests\": 0\n },\n \"engine\": {\n \"uses_new\": true,\n \"uses_new_full\": false\n },\n \"cluster\": {\n \"mode\": \"Leader\",\n \"all_nodes\": 1,\n \"alive_nodes\": 1,\n \"leaders_count\": 1,\n \"workers_count\": 0,\n \"nodes\": [\n {\n \"id\": \"node_15ac62ec3-3e0d-48c1-a8ea-15de97088e3c\",\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"version\": \"16.0.0-dev\",\n \"type\": \"Leader\",\n \"cpu_usage\": 10.992902320605205,\n \"load_average\": 44.38720703125,\n \"heap_used\": 527,\n \"heap_size\": 2048,\n \"relay\": true,\n \"tunnels\": 0\n }\n ]\n },\n \"entities\": {\n \"scripts\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"routes\": {\n \"count\": 24,\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"router_routes\": {\n \"count\": 27,\n \"http_clients\": {\n \"ahc\": 25,\n \"akka\": 2,\n \"netty\": 0,\n \"akka_ws\": 0\n },\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"route_compositions\": {\n \"count\": 1,\n \"plugins\": {\n \"min\": 1,\n \"max\": 1,\n \"avg\": 1\n },\n \"by_kind\": {\n \"global\": 1\n }\n },\n \"apikeys\": {\n \"count\": 6,\n \"by_kind\": {\n \"disabled\": 0,\n \"with_rotation\": 0,\n \"with_read_only\": 0,\n \"with_client_id_only\": 0,\n \"with_constrained_services\": 0,\n \"with_meta\": 2,\n \"with_tags\": 1\n },\n \"authorized_on\": {\n \"min\": 1,\n \"max\": 4,\n \"avg\": 2\n }\n },\n \"jwt_verifiers\": {\n \"count\": 6,\n \"by_strategy\": {\n \"pass_through\": 6\n },\n \"by_alg\": {\n \"HSAlgoSettings\": 6\n }\n },\n \"certificates\": {\n \"count\": 9,\n \"by_kind\": {\n \"auto_renew\": 6,\n \"exposed\": 6,\n \"client\": 1,\n \"keypair\": 1\n }\n },\n \"auth_modules\": {\n \"count\": 8,\n \"by_kind\": {\n \"basic\": 7,\n \"oauth2\": 1\n }\n },\n \"service_descriptors\": {\n \"count\": 3,\n \"plugins\": {\n \"old\": 0,\n \"new\": 0\n },\n \"by_kind\": {\n \"disabled\": 1,\n \"fault_injection\": 0,\n \"health_check\": 1,\n \"gzip\": 0,\n \"jwt\": 0,\n \"cors\": 1,\n \"auth\": 0,\n \"protocol\": 0,\n \"restrictions\": 0\n }\n },\n \"teams\": {\n \"count\": 2\n },\n \"tenants\": {\n \"count\": 2\n },\n \"service_groups\": {\n \"count\": 2\n },\n \"data_exporters\": {\n \"count\": 10,\n \"by_kind\": {\n \"elastic\": 5,\n \"file\": 2,\n \"metrics\": 1,\n \"console\": 1,\n \"s3\": 1\n }\n },\n \"otoroshi_admins\": {\n \"count\": 5,\n \"by_kind\": {\n \"simple\": 2,\n \"webauthn\": 3\n }\n },\n \"backoffice_sessions\": {\n \"count\": 1,\n \"by_kind\": {\n \"simple\": 1\n }\n },\n \"private_apps_sessions\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"tcp_services\": {\n \"count\": 0\n }\n },\n \"plugins_usage\": {\n \"cp:otoroshi.next.plugins.AdditionalHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.DisableHttp10\": 2,\n \"cp:otoroshi.next.plugins.OverrideHost\": 27,\n \"cp:otoroshi.next.plugins.TailscaleFetchCertificate\": 1,\n \"cp:otoroshi.next.plugins.OtoroshiInfos\": 6,\n \"cp:otoroshi.next.plugins.MissingHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.Redirection\": 2,\n \"cp:otoroshi.next.plugins.OtoroshiChallenge\": 5,\n \"cp:otoroshi.next.plugins.BuildMode\": 2,\n \"cp:otoroshi.next.plugins.XForwardedHeaders\": 2,\n \"cp:otoroshi.next.plugins.NgLegacyAuthModuleCall\": 2,\n \"cp:otoroshi.next.plugins.Cors\": 4,\n \"cp:otoroshi.next.plugins.OtoroshiHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.NgDefaultRequestBody\": 1,\n \"cp:otoroshi.next.plugins.NgHttpClientCache\": 1,\n \"cp:otoroshi.next.plugins.ReadOnlyCalls\": 2,\n \"cp:otoroshi.next.plugins.RemoveHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.JwtVerificationOnly\": 1,\n \"cp:otoroshi.next.plugins.ApikeyCalls\": 3,\n \"cp:otoroshi.next.plugins.WasmAccessValidator\": 3,\n \"cp:otoroshi.next.plugins.WasmBackend\": 3,\n \"cp:otoroshi.next.plugins.IpAddressAllowedList\": 2,\n \"cp:otoroshi.next.plugins.AuthModule\": 4,\n \"cp:otoroshi.next.plugins.RemoveHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.IpAddressBlockList\": 2,\n \"cp:otoroshi.next.proxy.ProxyEngine\": 1,\n \"cp:otoroshi.next.plugins.JwtVerification\": 3,\n \"cp:otoroshi.next.plugins.GzipResponseCompressor\": 2,\n \"cp:otoroshi.next.plugins.SendOtoroshiHeadersBack\": 3,\n \"cp:otoroshi.next.plugins.AdditionalHeadersIn\": 4,\n \"cp:otoroshi.next.plugins.SOAPAction\": 1,\n \"cp:otoroshi.next.plugins.NgLegacyApikeyCall\": 6,\n \"cp:otoroshi.next.plugins.ForceHttpsTraffic\": 2,\n \"cp:otoroshi.next.plugins.NgErrorRewriter\": 1,\n \"cp:otoroshi.next.plugins.MissingHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.MaintenanceMode\": 3,\n \"cp:otoroshi.next.plugins.RoutingRestrictions\": 2,\n \"cp:otoroshi.next.plugins.HeadersValidation\": 2\n }\n}\n```\n\n## Toggling\n\nAnonymous reporting can be toggled any time using :\n\n- the UI (Features > Danger zone > Send anonymous reports)\n- `otoroshi.anonymous-reporting.enabled` configuration\n- `OTOROSHI_ANONYMOUS_REPORTING_ENABLED` env variable\n"},{"name":"chaos-engineering.md","id":"/topics/chaos-engineering.md","url":"/topics/chaos-engineering.html","title":"Chaos engineering with the Snow Monkey","content":"# Chaos engineering with the Snow Monkey\n\nNihonzaru (the Snow Monkey) is the chaos engineering tool provided by Otoroshi. You can access it at `Settings (cog icon) / Snow Monkey`.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Chaos engineering\n\nOtoroshi offers some tools to introduce [chaos engineering](https://principlesofchaos.org/) in your everyday life. With chaos engineering, you will improve the resilience of your architecture by creating faults in production on running systems. With [Nihonzaru (the snow monkey)](https://en.wikipedia.org/wiki/Japanese_macaque) Otoroshi helps you to create faults on http request/response handled by Otoroshi. \n\n@@@ div { .centered-img }\n\n@@@\n\n## Settings\n\n@@@ div { .centered-img }\n\n@@@\n\nThe snow monkey let you define a few settings to work properly :\n\n* **Include user facing apps.**: you want to create fault in production, but maybe you don't want your users to enjoy some nice snow monkey generated error pages. Using this switch let you include of not user facing apps (ui apps). Each service descriptor has a `User facing app switch` that will be used by the snow monkey.\n* **Dry run**: when dry run is enabled, outages will be registered and will generate events and alerts (in the otoroshi eventing system) but requests won't be actualy impacted. It's a good way to prepare applications to the snow monkey habits\n* **Outage strategy**: Either `AllServicesPerGroup` or `OneServicePerGroup`. It means that only one service per group or all services per groups will have `n` outages (see next bullet point) during the snow monkey working period\n* **Outages per day**: during snow monkey working period, each service per group or one service per group will have only `n` outages registered \n* **Working period**: the snow monkey only works during a working period. Here you can defined when it starts and when it stops\n* **Outage duration**: here you can defined the bounds for the random outage duration when an outage is created on a service\n* **Impacted groups**: here you can define a list of service groups impacted by the snow monkey. If none is specified, then all service groups will be impacted\n\n## Faults\n\nWith the snow monkey, you can generate four types of faults\n\n* **Large request fault**: Add trailing bytes at the end of the request body (if one)\n* **Large response fault**: Add trailing bytes at the end of the response body\n* **Latency injection fault**: Add random response latency between two bounds\n* **Bad response injection fault**: Create predefined responses with custom headers, body and status code\n\nEach fault let you define a ratio for impacted requests. If you specify a ratio of `0.2`, then 20% of the requests for the impacte service will be impacted by this fault\n\n@@@ div { .centered-img }\n\n@@@\n\nThen you juste have to start the snow monkey and enjoy the show ;)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Current outages\n\nIn the last section of the snow monkey page, you can see current outages (per service), when they started, their duration, etc ...\n\n@@@ div { .centered-img }\n\n@@@"},{"name":"dev-portal.md","id":"/topics/dev-portal.md","url":"/topics/dev-portal.html","title":"Developer portal with Daikoku","content":"# Developer portal with Daikoku\n\nWhile Otoroshi is the perfect tool to manage your webapps in a technical point of view it lacked of business perspective. This is not the case anymore with Daikoku.\n\nWhile Otoroshi is a standalone, Daikoku is a developer portal which stands in front of Otoroshi and provides some business feature.\n\nWhether you want to use Daikoku for your public APIs, you want to monetize or with your private APIs to provide some documentation, facilitation and self-service feature, it will be the perfect portal for Otoroshi.\n\n@@@div { .plugin .platform }\n## Daikoku\n\nRun your first Daikoku with a simple jar or with one Docker command.\n\n\n
\nTry Daikoku \n
\n@link:[With jar](https://maif.github.io/daikoku/devmanual/getdaikoku/frombinaries.html)\n@link:[With Docker](https://maif.github.io/daikoku/devmanual/getdaikoku/fromdocker.html)\n@@@\n\n@@@div { .plugin .platform }\n## Contribute\n\nDaikoku is opensource, so all contributions are welcome.\n\n\n@link:[Show the repository](https://github.com/MAIF/daikoku)\n@@@\n\n@@@div { .plugin .platform }\n## Documentation\n\nDaikoku and its UI are fully documented.\n\n\n@link:[Read the documentation](https://maif.github.io/daikoku/devmanual/)\n@@@\n\n"},{"name":"engine.md","id":"/topics/engine.md","url":"/topics/engine.html","title":"Proxy engine","content":"# Proxy engine\n\nStarting from the `1.5.3` release, otoroshi offers a new plugin that implements the next generation of the proxy engine. \nThis engine has been designed based on our 5 years experience building, maintaining and running the previous one.\nIt tries to fix all the drawback we may have encountered during those years and highly improve performances, user experience, reporting and debugging capabilities. \n\nThe new engine is fully plugin oriented in order to spend CPU cycles only on useful stuff. You can enable this plugin only on some domain names so you can easily A/B test the new engine. The new proxy engine is designed to be more reactive and more efficient generally. It is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nStarting from version `16.0.0`, this engine will be enabled by default on any new otoroshi cluster. In a future version, the engine will be enabled for any new or exisiting otoroshi cluster.\n\n## Enabling the new engine\n\nBy default, all freshly started Otoroshi instances have the new proxy engine enabled by default, for the other, to enable the new proxy engine on an otoroshi instance, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration, enable it and in `domains` add the values of the desired domains (let say we want to use the new engine on `api.foo.bar`. It is possible to use `*.foo.bar` if that's what you want to do).\n\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the previous one.\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"debug_headers\" : false,\n \"reporting\": true,\n \"domains\" : [ \"api.foo.bar\" ],\n \"deny_domains\" : [ ],\n }\n}\n```\n\nif you need to enable global plugin with the new engine, you can add the following configuration in the `global plugins` configuration object \n\n```javascript\n{\n ...\n \"ng\": {\n \"slots\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.W3CTracing\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"baggage\": {\n \"foo\": \"bar\"\n }\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.RequestSinkWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.apikeys.ClientCredentialService\",\n \"ClientCredentialService\": {\n \"domain\": \"ccs-next-gen.oto.tools\",\n \"expiration\": 3600000,\n \"defaultKeyPair\": \"otoroshi-jwt-signing\",\n \"secure\": false\n }\n }\n }\n ]\n }\n ...\n}\n```\n\n## Entities\n\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `backends`: a list of targets to contact a backend\n\n## Entities sync\n\nA new behavior introduced for the new proxy engine is the entities sync job. To avoid unecessary operations on the underlying datastore when routing requests, a new job has been setup in otoroshi that synchronize the content of the datastore (at least a part of it) with an in-memory cache. Because of it, the propagation of changes between an admin api call and the actual result on routing can be longer than before. When a node creates, updates, or deletes an entity via the admin api, other nodes need to wait for the next poll to purge the old cached entity and start using the new one. You can change the interval between syncs with the configuration key `otoroshi.next.state-sync-interval` or the env. variable `OTOROSHI_NEXT_STATE_SYNC_INTERVAL`. The default value is `10000` and the unit is `milliseconds`\n\n@@@ warning\nBecause of entities sync, memory consumption of otoroshi will be significantly higher than previous versions. You can use `otoroshi.next.monitor-proxy-state-size=true` config (or `OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE` env. variable) to monitor the actual memory size of the entities cache. This will produce the `ng-proxy-state-size-monitoring` metric in standard otoroshi metrics\n@@@\n\n## Automatic conversion\n\nThe new engine uses new entities for its configuration, but in order to facilitate transition between the old world and the new world, all the `service descriptors` of an otoroshi instance are automatically converted live into `routes` periodically. Any `service descriptor` should still work as expected through the new engine while enjoying all the perks.\n\n@@@ warning\nthe experimental nature of the engine can imply unexpected behaviors for converted service descriptors\n@@@\n\n## Routing\n\nthe new proxy engine introduces a new router that has enhanced capabilities and performances. The router can handle thousands of routes declarations without compromising performances.\n\nThe new route allow routes to be matched on a combination of\n\n* hostname\n* path\n* header values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n* query param values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n\npatch matching works \n\n* exactly\n * matches `/api/foo` with `/api/foo` and not with `/api/foo/bar`\n* starting with value (default behavior, like the previous engine)\n * matches `/api/foo` with `/api/foo` but also with `/api/foo/bar`\n\npath matching can also include wildcard paths and even path params\n\n* plain old path: `subdomain.domain.tld/api/users`\n* wildcard path: `subdomain.domain.tld/api/users/*/bills`\n* named path params: `subdomain.domain.tld/api/users/:id/bills`\n* named regex path params: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n\nhostname matching works on \n\n* exact values\n * `subdomain.domain.tld`\n* wildcard values like\n * `*.domain.tld`\n * `subdomain.*.tld`\n\nas path matching can now include named path params, it is possible to perform a ful url rewrite on the target path like \n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Plugins\n\nthe new route entity defines a plugin pipline where any plugin can be enabled or not and can be active only on some paths. \nEach plugin slot in the pipeline holds the plugin id and the plugin configuration. \n\nYou can also enable debugging only on a plugin instance instead of the whole route (see [the debugging section](#debugging))\n\n```javascript\n{ \n ...\n \"plugins\" : [ {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.OverrideHost\",\n \"include\" : [ ],\n \"exclude\" : [ ],\n \"config\" : { }\n }, {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\" : [ ],\n \"exclude\" : [ \"/openapi.json\" ],\n \"config\" : { }\n } ]\n}\n```\n\nyou can find the list of built-in plugins @ref:[here](../plugins/built-in-plugins.md)\n\n## Using legacy plugins\n\nif you need to use legacy otoroshi plugins with the new engine, you can use several wrappers in order to do so\n\n* `otoroshi.next.plugins.wrappers.PreRoutingWrapper`\n* `otoroshi.next.plugins.wrappers.AccessValidatorWrapper`\n* `otoroshi.next.plugins.wrappers.RequestSinkWrapper`\n* `otoroshi.next.plugins.wrappers.RequestTransformerWrapper`\n* `otoroshi.next.plugins.wrappers.CompositeWrapper`\n\nto use it, just declare a plugin slot with the right wrapper and in the config, declare the `plugin` you want to use and its configuration like:\n\n```javascript\n{\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.PreRoutingWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.jwt.JwtUserExtractor\",\n \"JwtUserExtractor\": {\n \"verifier\" : \"$ref\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\": \"email\",\n \"metaPath\" : null\n }\n }\n}\n```\n\n## Reporting\n\nby default, any request hiting the new engine will generate an execution report with informations about how the request pipeline steps were performed. It is possible to export those reports as `RequestFlowReport` events using classical data exporter. By default, exporting for reports is not enabled, you must enable the `export_reporting` flag on a `route` or `service`.\n\n```javascript\n{\n \"@id\": \"8efac472-07bc-4a80-8d27-4236309d7d01\",\n \"@timestamp\": \"2022-02-15T09:51:25.402+01:00\",\n \"@type\": \"RequestFlowReport\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"service_548f13bb-a809-4b1d-9008-fae3b1851092\",\n \"@service\": \"demo-service\",\n \"@env\": \"prod\",\n \"route\": {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"report\": {\n \"id\" : \"ab73707b3-946b-4853-92d4-4c38bbaac6d6\",\n \"creation\" : \"2022-02-15T09:51:25.402+01:00\",\n \"termination\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 5,\n \"duration_ns\" : 5905522,\n \"overhead\" : 4,\n \"overhead_ns\" : 4223215,\n \"overhead_in\" : 2,\n \"overhead_in_ns\" : 2687750,\n \"overhead_out\" : 1,\n \"overhead_out_ns\" : 1535465,\n \"state\" : \"Successful\",\n \"steps\" : [ {\n \"task\" : \"start-handling\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 177430,\n \"ctx\" : null\n }, {\n \"task\" : \"check-concurrent-requests\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 145242,\n \"ctx\" : null\n }, {\n \"task\" : \"find-route\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 497119,\n \"ctx\" : {\n \"found_route\" : {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"matched_path\" : \"\",\n \"exact\" : true,\n \"params\" : { },\n \"matched_routes\" : [ \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\" ]\n }\n }, {\n \"task\" : \"compute-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 105151,\n \"ctx\" : {\n \"disabled_plugins\" : [ ],\n \"filtered_plugins\" : [ ]\n }\n }, {\n \"task\" : \"tenant-check\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 26097,\n \"ctx\" : null\n }, {\n \"task\" : \"check-global-maintenance\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 14132,\n \"ctx\" : null\n }, {\n \"task\" : \"call-before-request-callbacks\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 56671,\n \"ctx\" : null\n }, {\n \"task\" : \"extract-tracking-id\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 5207,\n \"ctx\" : null\n }, {\n \"task\" : \"call-pre-route-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 39786,\n \"ctx\" : null\n }, {\n \"task\" : \"call-access-validator-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 25311,\n \"ctx\" : null\n }, {\n \"task\" : \"enforce-global-limits\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 296617,\n \"ctx\" : {\n \"remaining_quotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 10000000,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 10000000,\n \"remainingCallsPerDay\" : 10000000,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 10000000,\n \"remainingCallsPerMonth\" : 10000000\n }\n }\n }, {\n \"task\" : \"choose-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 368899,\n \"ctx\" : {\n \"backend\" : {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n }\n }, {\n \"task\" : \"transform-request\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 506363,\n \"ctx\" : null\n }, {\n \"task\" : \"call-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 2,\n \"duration_ns\" : 2163470,\n \"ctx\" : null\n }, {\n \"task\" : \"transform-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 279887,\n \"ctx\" : null\n }, {\n \"task\" : \"stream-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 382952,\n \"ctx\" : null\n }, {\n \"task\" : \"trigger-analytics\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 812036,\n \"ctx\" : null\n }, {\n \"task\" : \"request-success\",\n \"start\" : 1644915085408,\n \"start_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 0,\n \"ctx\" : null\n } ]\n }\n}\n```\n\n## Debugging\n\nwith the new reporting capabilities, the new engine also have debugging capabilities built in. In you enable the `debug_flow` flag on a route (or service), the resulting `RequestFlowReport` will be enriched with contextual informations between each plugins of the route plugin pipeline\n\n@@@ note\nyou can also use the `Try it` feature of the new route designer UI to get debug reports automatically for a specific call\n@@@\n\n## HTTP traffic capture\n\nusing the `capture` flag, a `TrafficCaptureEvent` is generated for each http request/response. This event will contains request and response body. Those events can be exported using @ref:[data exporters](../entities/data-exporters.md) as usual. You can also use the @ref:[GoReplay file exporter](../entities/data-exporters.md#goreplay-file) that is specifically designed to ingest those events and create [GoReplay](https://goreplay.org/) files (`.gor`)\n\n@@@ warning\nthis feature can have actual impact on CPU and RAM consumption\n@@@\n\n```json\n{\n \"@id\": \"d5998b0c4-cb08-43e6-9921-27472c7a56e0\",\n \"@timestamp\": 1651828801115,\n \"@type\": \"TrafficCaptureEvent\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"@service\": \"test-server\",\n \"@env\": \"prod\",\n \"route\": {\n \"id\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"name\": \"test-server\"\n },\n \"request\": {\n \"id\": \"152250645825034725600000\",\n \"int_id\": 115,\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"test-server-next-gen.oto.tools:9999\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\",\n \"Remote-Address\": \"127.0.0.1:57660\",\n \"Timeout-Access\": \"\",\n \"Raw-Request-URI\": \"/\",\n \"Tls-Session-Info\": \"Session(1651828041285|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"path\": \"/\",\n \"domain\": null,\n \"http_only\": true,\n \"max_age\": null,\n \"secure\": false,\n \"same_site\": null\n }\n ],\n \"tls\": false,\n \"uri\": \"/\",\n \"path\": \"/\",\n \"version\": \"HTTP/1.1\",\n \"has_body\": true,\n \"remote\": \"127.0.0.1\",\n \"client_cert_chain\": null,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_request\": {\n \"url\": \"http://localhost:3000/\",\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"localhost\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\"\n },\n \"version\": \"HTTP/1.1\",\n \"client_cert_chain\": null,\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"domain\": null,\n \"path\": \"/\",\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": true\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"int_id\": 33,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_response\": {\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"response\": {\n \"id\": \"152250645825034725600000\",\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"user-agent-details\": null,\n \"origin-details\": null,\n \"instance-number\": 0,\n \"instance-name\": \"dev\",\n \"instance-zone\": \"local\",\n \"instance-region\": \"local\",\n \"instance-dc\": \"local\",\n \"instance-provider\": \"local\",\n \"instance-rack\": \"local\",\n \"cluster-mode\": \"Leader\",\n \"cluster-name\": \"otoroshi-leader-9hnv5HUXpbCZD7Ee\"\n}\n```\n\n## openapi import\n\nas the new router offers possibility to match exactly on a single path and a single method, and with the help of the `service` entity, it is now pretty easy to import openapi document as `route-compositions` entities. To do that, a new api has been made available to perform the translation. Be aware that this api **DOES NOT** save the entity and just return the result of the translation. \n\n```sh\ncurl -X POST \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/route-compositions/_openapi' \\\n -d '{\"domain\":\"oto-api-proxy.oto.tools\",\"openapi\":\"https://raw.githubusercontent.com/MAIF/otoroshi/master/otoroshi/public/openapi.json\"}'\n```\n\n@@@ div { .centered-img }\n\n@@@\n\n"},{"name":"events-and-analytics.md","id":"/topics/events-and-analytics.md","url":"/topics/events-and-analytics.html","title":"Events and analytics","content":"# Events and analytics\n\nOtoroshi is a solution fully traced : calls to services, access to UI, creation of resources, etc.\n\n@@@ warning\nYou have to use [Elastic](https://www.elastic.co) to enable analytics features in Otoroshi\n@@@\n\n## Events\n\n* Analytics event\n* Gateway event\n* TCP event\n* Healthcheck event\n\n## Event log\n\nOtoroshi can read his own exported events from an Elasticsearch instance, set up in the danger zone. Theses events are available from the UI, at the following route: `https://xxxxx/bo/dashboard/events`.\n\nThe `Global events` page display all events of **GatewayEvent** type. This page is a way to quickly read an interval of events and can be used in addition of a Kibana instance.\n\nFor each event, a list of information will be displayed and an additional button `content` to watch the full content of the event, at the JSON format. \n\n## Alerts \n\n* `MaxConcurrentRequestReachedAlert`: happening when the handled requests number are greater than the limit of concurrent requests indicated in the global configuration of Otoroshi\n* `CircuitBreakerOpenedAlert`: happening when the circuit breaker pass from closed to opened\n* `CircuitBreakerClosedAlert`: happening when the circuit breaker pass from opened to closed\n* `SessionDiscardedAlert`: send when an admin discarded an admin sessions\n* `SessionsDiscardedAlert`: send when an admin discarded all admin sessions\n* `PanicModeAlert`: send when panic mode is enabled\n* `OtoroshiExportAlert`: send when otoroshi global configuration is exported\n* `U2FAdminDeletedAlert`: send when an admin has deleted an other admin user\n* `BlackListedBackOfficeUserAlert`: send when a blacklisted user has tried to acccess to the UI\n* `AdminLoggedInAlert`: send when an user admin has logged to the UI\n* `AdminFirstLogin`: send when an user admin has successfully logged to the UI for the first time\n* `AdminLoggedOutAlert`: send when an user admin has logged out from Otoroshi\n* `GlobalConfigModification`: send when an user amdin has changed the global configuration of Otoroshi\n* `RevokedApiKeyUsageAlert`: send when an user admin has revoked an apikey\n* `ServiceGroupCreatedAlert`: send when an user admin has created a service group\n* `ServiceGroupUpdatedAlert`: send when an user admin has updated a service group\n* `ServiceGroupDeletedAlert`: send when an user admin has deleted a service group\n* `ServiceCreatedAlert`: send when an user admin has created a tcp service\n* `ServiceUpdatedAlert`: send when an user admin has updated a tcp service\n* `ServiceDeletedAlert`: send when an user admin has deleted a tcp service\n* `ApiKeyCreatedAlert`: send when an user admin has crated a new apikey\n* `ApiKeyUpdatedAlert`: send when an user admin has updated a new apikey\n* `ApiKeyDeletedAlert`: send when an user admin has deleted a new apikey\n\n## Audit\n\nWith Otoroshi, any admin action and any sucpicious/alert action is recorded. These records are stored in Otoroshi’s datastore (only the last n records, defined by the `otoroshi.events.maxSize` config key). All the records can be send through the analytics mechanism (WebHook, Kafka, Elastic) for external and/or further usage. We recommand sending away those records for security reasons.\n\nOtoroshi keep the following list of information for each executed action:\n\n* `Date`: moment of the action\n* `User`: name of the owner\n* `From`: IP of the concerned user\n* `Action`: action performed by the person. The possible actions are:\n\n * `ACCESS_APIKEY`: User accessed a apikey\n * `ACCESS_ALL_APIKEYS`: User accessed all apikeys\n * `CREATE_APIKEY`: User created a apikey\n * `UPDATE_APIKEY`: User updated a apikey\n * `DELETE_APIKEY`: User deleted a apikey\n * `ACCESS_AUTH_MODULE`: User accessed an Auth. module\n * `ACCESS_ALL_AUTH_MODULES`: User accessed all Auth. modules\n * `CREATE_AUTH_MODULE`: User created an Auth. module\n * `UPDATE_AUTH_MODULE`: User updated an Auth. module\n * `DELETE_AUTH_MODULE`: User deleted an Auth. module\n * `ACCESS_CERTIFICATE`: User accessed a certificate\n * `ACCESS_ALL_CERTIFICATES`: User accessed all certificates\n * `CREATE_CERTIFICATE`: User created a certificate\n * `UPDATE_CERTIFICATE`: User updated a certificate\n * `DELETE_CERTIFICATE`: User deleted a certificate\n * `ACCESS_CLIENT_CERT_VALIDATOR`: User accessed a client cert. validator\n * `ACCESS_ALL_CLIENT_CERT_VALIDATORS`: User accessed all client cert. validators\n * `CREATE_CLIENT_CERT_VALIDATOR`: User created a client cert. validator\n * `UPDATE_CLIENT_CERT_VALIDATOR`: User updated a client cert. validator\n * `DELETE_CLIENT_CERT_VALIDATOR`: User deleted a client cert. validator\n * `ACCESS_DATA_EXPORTER_CONFIG`: User accessed a data exporter config\n * `ACCESS_ALL_DATA_EXPORTER_CONFIG`: User accessed all data exporter config\n * `CREATE_DATA_EXPORTER_CONFIG`: User created a data exporter config\n * `UPDATE_DATA_EXPORTER_CONFIG`: User updated a data exporter config\n * `DELETE_DATA_EXPORTER_CONFIG`: User deleted a data exporter config\n * `ACCESS_GLOBAL_JWT_VERIFIER`: User accessed a global jwt verifier\n * `ACCESS_ALL_GLOBAL_JWT_VERIFIERS`: User accessed all global jwt verifiers\n * `CREATE_GLOBAL_JWT_VERIFIER`: User created a global jwt verifier\n * `UPDATE_GLOBAL_JWT_VERIFIER`: User updated a global jwt verifier\n * `DELETE_GLOBAL_JWT_VERIFIER`: User deleted a global jwt verifier\n * `ACCESS_SCRIPT`: User accessed a script\n * `ACCESS_ALL_SCRIPTS`: User accessed all scripts\n * `CREATE_SCRIPT`: User created a script\n * `UPDATE_SCRIPT`: User updated a script\n * `DELETE_SCRIPT`: User deleted a Script\n * `ACCESS_SERVICES_GROUP`: User accessed a service group\n * `ACCESS_ALL_SERVICES_GROUPS`: User accessed all services groups\n * `CREATE_SERVICE_GROUP`: User created a service group\n * `UPDATE_SERVICE_GROUP`: User updated a service group\n * `DELETE_SERVICE_GROUP`: User deleted a service group\n * `ACCESS_SERVICES_FROM_SERVICES_GROUP`: User accessed all services from a services group\n * `ACCESS_TCP_SERVICE`: User accessed a tcp service\n * `ACCESS_ALL_TCP_SERVICES`: User accessed all tcp services\n * `CREATE_TCP_SERVICE`: User created a tcp service\n * `UPDATE_TCP_SERVICE`: User updated a tcp service\n * `DELETE_TCP_SERVICE`: User deleted a tcp service\n * `ACCESS_TEAM`: User accessed a Team\n * `ACCESS_ALL_TEAMS`: User accessed all teams\n * `CREATE_TEAM`: User created a team\n * `UPDATE_TEAM`: User updated a team\n * `DELETE_TEAM`: User deleted a team\n * `ACCESS_TENANT`: User accessed a Tenant\n * `ACCESS_ALL_TENANTS`: User accessed all tenants\n * `CREATE_TENANT`: User created a tenant\n * `UPDATE_TENANT`: User updated a tenant\n * `DELETE_TENANT`: User deleted a tenant\n * `SERVICESEARCH`: User searched for a service\n * `ACTIVATE_PANIC_MODE`: Admin activated panic mode\n\n\n* `Message`: explicit message about the action (example: the `SERVICESEARCH` action happened when an `user searched for a service`)\n* `Content`: all information at JSON format\n\n## Global metrics\n\nThe global metrics are displayed on the index page of the Otoroshi UI. Otoroshi provides information about :\n\n* the number of requests served\n* the amount of data received and sended\n* the number of concurrent requests\n* the number of requests per second\n* the current overhead\n\nMore metrics can be found on the **Global analytics** page (available at https://xxxxxx/bo/dashboard/stats).\n\n## Monitoring services\n\nOnce you have declared services, you can monitor them with Otoroshi. \n\nLet's starting by setup Otoroshi to push events to an elastic cluster via a data exporter. Then you will can setup Otoroshi events read from an elastic cluster. Go to `settings (cog icon) / Danger Zone` and expand the `Analytics: Elastic cluster (read)` section.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service healthcheck\n\nIf you have defined an health check URL in the service descriptor, you can access the health check page from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service live stats\n\nYou can also monitor live stats like total of served request, average response time, average overhead, etc. The live stats page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service analytics\n\nYou can also get some aggregated metrics. The analytics page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n## New proxy engine\n\n### Debug reporting\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `debug_flow` flag, events of type `RequestFlowReport` are generated\n\n### Traffic capture\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `capture` flag, events of type `TrafficCaptureEvent` are generated. It contains everything that compose otoroshi input http request and output http responses\n"},{"name":"expression-language.md","id":"/topics/expression-language.md","url":"/topics/expression-language.html","title":"Expression language","content":"# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```"},{"name":"graphql-composer.md","id":"/topics/graphql-composer.md","url":"/topics/graphql-composer.html","title":"GraphQL Composer Plugin","content":"# GraphQL Composer Plugin\n\n
\nRoute plugins:\nGraphQL Composer\n
\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\n> GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.\n[Official GraphQL website](https://graphql.org/)\n\nAPIs RESTful and GraphQL development has become one of the most popular activities for companies as well as users in recent times. In fast scaling companies, the multiplication of clients can cause the number of API needs to grow at scale.\n\nOtoroshi comes with a solution to create and meet your customers' needs without constantly creating and recreating APIs: the `GraphQL composer plugin`. The GraphQL Composer is an useful plugin to build an GraphQL API from multiples differents sources. These sources can be REST apis, GraphQL api or anything that supports the HTTP protocol. In fact, the plugin can define and expose for each of your client a specific GraphQL schema, which only corresponds to the needs of the customers.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n## Tutorial\n\nLet's take an example to get a better view of this plugin. We want to build a schema with two types: \n\n* an user with a name and a password \n* an country with a name and its users.\n\nTo build this schema, we need to use three custom directives. A `directive` decorates part of a GraphQL schema or operation with additional configuration. Directives are preceded by the @ character, like so:\n\n* @ref:[rest](#directives) : to call a http rest service with dynamic path params\n* @ref:[permission](#directives) : to restrict the access to the sensitive field\n* @ref:[graphql](#directives) : to call a graphQL service by passing a url and the associated query\n\nThe final schema of our tutorial should look like this\n```graphql\ntype Country {\n name: String\n users: [User] @rest(url: \"http://localhost:5000/countries/${item.name}/users\")\n}\n\ntype User {\n name: String\n password: String @password(value: \"ADMIN\")\n}\n\ntype Query {\n users: [User] @rest(url: \"http://localhost:5000/users\", paginate: true)\n user(id: String): User @rest(url: \"http://localhost:5000/users/${params.id}\")\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com\", query: \"{ countries { name }}\", paginate: true)\n}\n```\n\nNow you know the GraphQL Composer basics and how it works, let's configure it on our project:\n\n* create a route using the new Otoroshi router describing the previous countries API\n* add the GraphQL composer plugin\n* configure the plugin with the schema\n* try to call it\n\n@@@ div { .centered-img }\n\n@@@\n\n### Setup environment\n\nFirst of all, we need to download the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v1.5.15/otoroshi.jar'\n```\n\nNow, just run the command belows to start the Otoroshi, and look the console to see the output.\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow, login to [the UI](http://otoroshi.oto.tools:8080) with \n```sh\nuser = admin@otoroshi.io\npassword = password\n```\n\n### Create our countries API\n\nFirst thing to do in any new API is of course creating a `route`. We need 4 informations which are:\n\n* name: `My countries API`\n* frontend: exposed on `countries-api.oto.tools`\n* plugins: the list of plugins with only the `GraphQL composer` plugin\n\nLet's make a request call through the Otoroshi Admin API (with the default apikey), like the example below\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\"countries.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\"\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\n### Build the countries API \n\nLet's continue our API by patching the configuration of the GraphQL plugin with the complete schema.\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/api/routes/countries-api' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\n \"countries.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\",\n \"config\": {\n \"schema\": \"type Country {\\n name: String\\n users: [User] @rest(url: \\\"http://localhost:8181/countries/${item.name}/users\\\", headers: \\\"{}\\\")\\n}\\n\\ntype Query {\\n users: [User] @rest(url: \\\"http://localhost:8181/users\\\", paginate: true, headers: \\\"{}\\\")\\n user(id: String): User @rest(url: \\\"http://localhost:8181/users/${params.id}\\\")\\n countries: [Country] @graphql(url: \\\"https://countries.trevorblades.com\\\", query: \\\"{ countries { name }}\\\", paginate: true)\\ntype User {\\n name: String\\n password: String }\\n\"\n }\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThe route is created but it expects an API, exposed on the localhost:8181, to work. \n\nLet's create this simple API which returns a list of users and of countries. This should look like the following snippet.\nThe API uses express as http server.\n\n```js\nconst express = require('express')\n\nconst app = express()\n\nconst users = [\n {\n name: 'Joe',\n password: 'password'\n },\n {\n name: 'John',\n password: 'password2'\n }\n]\n\nconst countries = [\n {\n name: 'Andorra',\n users: [users[0]]\n },\n {\n name: 'United Arab Emirates',\n users: [users[1]]\n }\n]\n\napp.get('/users', (_, res) => {\n return res.json(users)\n})\n\napp.get(`/users/:name`, (req, res) => {\n res.json(users.find(u => u.name === req.params.name))\n})\n\napp.get('/countries/:id/users', (req, res) => {\n const country = countries.find(c => c.name === req.params.id)\n\n if (country) \n return res.json(country.users)\n else \n return res.json([])\n})\n\napp.listen(8181, () => {\n console.log(`Listening on 8181`)\n});\n\n```\n\nLet's try to make a first call to our countries API.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": [\n { \n \"name\":\"Andorra\",\n \"users\": [\n { \"name\":\"Joe\" }\n ]\n }\n ]\n }\n}\n```\n\nThe call graph should looks like\n\n```\n1. Calls https://countries.trevorblades.com\n2. For each country:\n - extract the field name\n - calls http://localhost:8181/countries/${country}/users to get the list of users for this country\n```\n\nYou may have noticed that we added an argument at the end of the graphql directive named `paginate`. It enabled the paging for the client accepting limit and offset parameters. These parameters are used by the plugin to filter and reduce the content.\n\nLet's make a new call that does not accept any country.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": []\n }\n}\n```\n\nLet's move on to the next section to secure sensitive field of our API.\n\n### Basics of permissions \n\nThe permission directives has been created to protect the fields of the graphql schema. The validation process starts by create a `context` for all incoming requests, based on the list of paths defined in the permissions field of the plugin. The permissions paths can refer to the request data (url, headers, etc), user credentials (api key, etc) and informations about the matched route. Then the process can validate that the value or values are present in the `context`.\n\n@@@div { .simple-block }\n\n
\nPermission\n\n
\n\n*Arguments : value and unauthorized_value*\n\nThe permission directive can be used to secure a field on **one** value. The directive checks that a specific value is present in the `context`.\n\nTwo arguments are available, the first, named `value`, is required and designates the value found. The second optional value, `unauthorized_value`, can be used to indicates, in the outcoming response, the rejection message.\n\n**Example**\n```js\ntype User {\n id: String @permission(\n value: \"FOO\", \n unauthorized_value: \"You're not authorized to get this field\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nAll permissions\n\n
\n\n*Arguments : values and unauthorized_value*\n\nThis directive is presumably the same as the previous one except that it takes a list of values.\n\n**Example**\n```js\ntype User {\n id: String @allpermissions(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO and BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nOne permissions of\n\n
\n*Arguments : values and unauthorized_value*\n\nThis directive takes a list of values and validate that one of them is in the context.\n\n**Example**\n```js\ntype User {\n id: String @onePermissionsOf(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO or BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nAuthorize\n\n
\n\n*Arguments : path, value and unauthorized_value*\n\nThe authorize directive has one more required argument, named `path`, which indicates the path to value, in the context. Unlike the last three directives, the authorize directive doesn't search in the entire context but at the specified path.\n\n**Example**\n```js\ntype User {\n id: String @authorize(\n path: \"$.raw_request.headers.foo\", \n value: \"BAR\", \n unauthorized_value: \"Bar could not be found in the foo header\")\n}\n```\n@@@\n\nLet's restrict the password field to the users that comes with a `role` header of the value `ADMIN`.\n\n1. Patch the configuration of the API by adding the permissions in the configuration of the plugin.\n```json\n...\n \"permissions\": [\"$.raw_request.headers.role\"]\n...\n```\n\n1. Add an directive on the password field in the schema\n```graphql\ntype User {\n name: String\n password: String @permission(value: \"ADMIN\")\n}\n```\n\nLet's make a call with the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: ADMIN'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nNow try to change the value of the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: USER'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nThe error message should look like \n\n```json\n{\n \"errors\": [\n {\n \"message\": \"You're not authorized\",\n \"path\": [\n \"countries\",\n 0,\n \"users\",\n 0,\n \"password\"\n ],\n ...\n }\n ]\n}\n```\n\n\n# Glossary\n\n## Directives\n\n@@@div { .simple-block }\n\n
\nRest\n\n
\n\n*Arguments : url, method, headers, timeout, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to expose servers that communicate using the http protocol. The only required argument is the `url`.\n\n**Example**\n```js\ntype Query {\n users(limit: Int, offset: Int): [User] @rest(url: \"http://foo.oto.tools/users\", method: \"GET\")\n}\n```\n\nIt can be placed on the field of a query and type. To custom your url queries, you can use the path parameter and another field with respectively, `params` and `item` variables.\n\n**Example**\n```js\ntype Country {\n name: String\n phone: String\n users: [User] @rest(url: \"http://foo.oto.tools/users/${item.name}\")\n}\n\ntype Query {\n user(id: String): User @rest(url: \"http://foo.oto.tools/users/${params.id}\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nGraphQL\n\n
\n\n*Arguments : url, method, headers, timeout, query, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to call an other graphql server.\n\nThe required argument are the `url` and the `query`.\n\n**Example**\n```js\ntype Query {\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com/\", query: \"{ countries { name phone }}\")\n}\n\ntype Country {\n name: String\n phone: String\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nSoap\n\n
\n*Arguments: all following arguments*\n\nThe soap directive is used to call a soap service. \n\n```js\ntype Query {\n randomNumber: String @soap(\n jq_response_filter: \".[\\\"soap:Envelope\\\"] | .[\\\"soap:Body\\\"] | .[\\\"m:NumberToWordsResponse\\\"] | .[\\\"m:NumberToWordsResult\\\"]\", \n url: \"https://www.dataaccess.com/webservicesserver/numberconversion.wso\", \n envelope: \" \\n \\n \\n \\n 12 \\n \\n \\n\")\n}\n```\n\n\n##### Specific arguments\n\n| Argument | Type | Optional | Default value |\n| --------------------------- | --------- | -------- | ------------- |\n| envelope | *STRING* | Required | |\n| url | *STRING* | x | |\n| action | *STRING* | x | |\n| preserve_query | *BOOLEAN* | Required | true |\n| charset | *STRING* | x | |\n| convert_request_body_to_xml | *BOOLEAN* | Required | true |\n| jq_request_filter | *STRING* | x | |\n| jq_response_filter | *STRING* | x | |\n\n@@@\n\n@@@div { .simple-block }\n\n
\nJSON\n\n
\n*Arguments: path, json, paginate*\n\nThe json directive can be used to expose static data or mocked data. The first usage is to defined a raw stringify JSON in the `data` argument. The second usage is to set data in the predefined field of the GraphQL plugin composer and to specify a path in the `path` argument.\n\n**Example**\n```js\ntype Query {\n users_from_raw_data: [User] @json(data: \"[{\\\"firstname\\\":\\\"Foo\\\",\\\"name\\\":\\\"Bar\\\"}]\")\n users_from_predefined_data: [User] @json(path: \"users\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nMock\n\n
\n*Arguments: url*\n\nThe mock directive is to used with the Mock Responses Plugin, also named `Charlatan`. This directive can be interesting to mock your schema and start to use your Otoroshi route before starting to develop the underlying service.\n\n**Example**\n```js\ntype Query {\n users: @mock(url: \"/users\")\n}\n```\n\nThis example supposes that the Mock Responses plugin is set on the route's feed, and that an endpoint `/users` is available.\n\n@@@\n\n### List of directive arguments\n\n| Argument | Type | Optional | Default value |\n| ------------------ | ---------------- | --------------------------- | ------------- |\n| url | *STRING* | | |\n| method | *STRING* | x | GET |\n| headers | *STRING* | x | |\n| timeout | *INT* | x | 5000 |\n| data | *STRING* | x | |\n| path | *STRING* | x (only for json directive) | |\n| query | *STRING* | x | |\n| response_path | *STRING* | x | |\n| response_filter | *STRING* | x | |\n| limit | *INT* | x | |\n| offset | *INT* | x | |\n| value | *STRING* | | |\n| values | LIST of *STRING* | |\n| path | *STRING* | | |\n| paginate | *BOOLEAN* | x | |\n| unauthorized_value | *STRING* | x (only for permissions directive) | |\n"},{"name":"green-score.md","id":"/topics/green-score.md","url":"/topics/green-score.html","title":"Green Score","content":"# Green Score\n\nThe Green Score provide aggregated, quantitative data about the performance and behavior of an API over time. It is an aggregation of static and dynamic values that are coming from the usage of routes in Otoroshi. The main objective is to advise users on the consumption of their APIs and services.\n\n\n\nOtoroshi has a complete integration of the collective rules, divided into four concerns: **Architecture**, **Design**, **Usage** and **Logs retention**. The 6000 score points are spread over the four parts and a final note is given for each group of routes.\n\nThe API green score is available on 16.9.0 or later version of Otoroshi. You can find the feature on the search bar of your Otoroshi UI or directly in the sidebar by clicking on **Green score**.\n\nTo start the process, click on Add New Group, give a name and select a first route to audit. After clicking on the hammer icon, you can select the rules respected by your route. Before saving, you can adjust the values used to calculate the dynamic score. These thresholds are used to calculate a second green score depending on the amount of data you want not to exceed from your downstream service and the following other values: \n\n* **Overhead**: Otoroshi's calculation time to handle the request and response\n* **Duration**: the complete duration from the recpetion of the request by Otoroshi until the client gets a response\n* **Backend duration**: the time required for downstream service to respond to Otoroshi\n* **Calls**: the rate of calls by seconds\n* **Data in**: the amount of data received by the downstream service\n* **Data out**: the amount of data produced by the downstream service\n* **Headers in**: the amount of headers received by the downstream service\n* **Headers out**: the amount of headers produced by the downstream service\n\nThe Green Score works for all architectures, including simple leader or more advanced concept like [clustering](https://maif.github.io/otoroshi/manual/deploy/clustering.html)."},{"name":"http3.md","id":"/topics/http3.md","url":"/topics/http3.html","title":"HTTP3 support","content":"# HTTP3 support\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nHTTP3 server and client previews are available in otoroshi since version 1.5.14\n\n\n## Server\n\nto enable http3 server preview, you need to enable the following flags\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048\n```\n\nthen you will be able to send HTTP3 request on port 10048. For instance, using [quiche-client](https://github.com/cloudflare/quiche)\n\n```sh\ncargo run --bin quiche-client -- --no-verify 'https://my-service.oto.tools:10048'\n```\n\n## Client\n\nto consume services exposed with HTTP3, just select the `HTTP/3.0` protocol in the backend target."},{"name":"index.md","id":"/topics/index.md","url":"/topics/index.html","title":"Detailed topics","content":"# Detailed topics\n\nIn this sections, you will find informations about various Otoroshi topics \n\n* @ref:[Proxy engine](./engine.md)\n* @ref:[WASM support](./wasm-usage.md)\n* @ref:[Chaos engineering](./chaos-engineering.md)\n* @ref:[TLS](./tls.md)\n* @ref:[Otoroshi's PKI](./pki.md)\n* @ref:[Monitoring](./monitoring.md)\n* @ref:[Events and analytics](./events-and-analytics.md)\n* @ref:[Developer portal with Daikoku](./dev-portal.md)\n* @ref:[Sessions management](./sessions-mgmt.md)\n* @ref:[The Otoroshi communication protocol](./otoroshi-protocol.md)\n* @ref:[Expression language](./expression-language.md)\n* @ref:[Otoroshi user rights](./user-rights.md)\n* @ref:[GraphQL composer](./graphql-composer.md)\n* @ref:[Secret vaults](./secrets.md)\n* @ref:[Otoroshi tunnels](./tunnels.md)\n* @ref:[Relay routing](./relay-routing.md)\n* @ref:[Alternative http backend](./netty-server.md)\n* @ref:[HTTP3 support](./http3.md)\n* @ref:[Anonymous reporting](./anonymous-reporting.md)\n* @ref:[OpenTelemetry support](./opentelemetry.md)\n* @ref:[Green score](./green-score.md)\n\n@@@ index\n\n* [Proxy engine](./engine.md)\n* [WASM support](./wasm-usage.md)\n* [Chaos engineering](./chaos-engineering.md)\n* [TLS](./tls.md)\n* [Otoroshi's PKI](./pki.md)\n* [Monitoring](./monitoring.md)\n* [Events and analytics](./events-and-analytics.md)\n* [Developer portal with Daikoku](./dev-portal.md)\n* [Sessions management](./sessions-mgmt.md)\n* [The Otoroshi communication protocol](./otoroshi-protocol.md)\n* [Expression language](./expression-language.md)\n* [Otoroshi user rights](./user-rights.md)\n* [GraphQL composer](./graphql-composer.md)\n* [Secret vaults](./secrets.md)\n* [Otoroshi tunnels](./tunnels.md)\n* [Relay routing](./relay-routing.md)\n* [Alternative http backend](./netty-server.md)\n* [HTTP3 support](./http3.md)\n* [Anonymous reporting](./anonymous-reporting.md)\n* [OpenTelemetry support](./opentelemetry.md)\n* [Green score](./green-score.md)\n \n@@@\n"},{"name":"monitoring.md","id":"/topics/monitoring.md","url":"/topics/monitoring.html","title":"Monitoring","content":"# Monitoring\n\nThe Otoroshi API exposes two endpoints to know more about instance health. All the following endpoint are exposed on the instance host through it's ip address. It is also exposed on the otoroshi api hostname and the otoroshi backoffice hostname\n\n* `/health`: the health of the Otoroshi instance\n* `/metrics`: the metrics of the Otoroshi instance, either in JSON or Prometheus format using the `Accept` header (with `application/json` / `application/prometheus` values) or the `format` query param (with `json` or `prometheus` values)\n* `/live`: returns an http 200 response `{\"live\": true}` when the service is alive\n* `/ready`: return an http 200 response `{\"ready\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"ready\": false}`\n* `/startup`: return an http 200 response `{\"started\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"started\": false}`\n\nthose routes are also available on any hostname leading to otoroshi with a twist in the URL\n\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/health\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/metrics\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/live\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/ready\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/startup\n\n## Endpoints security\n\nThe two endpoints are exposed publicly on the Otoroshi admin api. But you can remove the corresponding public pattern and query the endpoints using standard apikeys. If you don't want to use apikeys but don't want to expose the endpoints publicly, you can defined two config. variables (`otoroshi.health.accessKey` or `HEALTH_ACCESS_KEY` and `otoroshi.metrics.accessKey` or `OTOROSHI_METRICS_ACCESS_KEY`) that will hold an access key for the endpoints. Then you can call the endpoints with an `access_key` query param with the value defined in the config. If you don't defined `otoroshi.metrics.accessKey` but define `otoroshi.health.accessKey`, `otoroshi.metrics.accessKey` will have the value of `otoroshi.health.accessKey`.\n \n## Examples\n\nlet say `otoroshi.health.accessKey` has value `MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY`\n\n```sh\n$ curl http://otoroshi-api.oto.tools:8080/health\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"otoroshi\":\"healthy\",\"datastore\":\"healthy\"}\n\n$ curl -H 'Accept: application/json' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"version\":\"4.0.0\",\"gauges\":{\"attr.app.commit\":{\"value\":\"xxxx\"},\"attr.app.id\":{\"value\":\"xxxx\"},\"attr.cluster.mode\":{\"value\":\"Leader\"},\"attr.cluster.name\":{\"value\":\"otoroshi-leader-0\"},\"attr.instance.env\":{\"value\":\"prod\"},\"attr.instance.id\":{\"value\":\"xxxx\"},\"attr.instance.number\":{\"value\":\"0\"},\"attr.jvm.cpu.usage\":{\"value\":136},\"attr.jvm.heap.size\":{\"value\":1409},\"attr.jvm.heap.used\":{\"value\":112},\"internals.0.concurrent-requests\":{\"value\":1},\"internals.global.throttling-quotas\":{\"value\":2},\"jvm.attr.name\":{\"value\":\"2085@xxxx\"},\"jvm.attr.uptime\":{\"value\":2296900},\"jvm.attr.vendor\":{\"value\":\"JDK11\"},\"jvm.gc.PS-MarkSweep.count\":{\"value\":3},\"jvm.gc.PS-MarkSweep.time\":{\"value\":261},\"jvm.gc.PS-Scavenge.count\":{\"value\":12},\"jvm.gc.PS-Scavenge.time\":{\"value\":161},\"jvm.memory.heap.committed\":{\"value\":1477967872},\"jvm.memory.heap.init\":{\"value\":1690304512},\"jvm.memory.heap.max\":{\"value\":3005218816},\"jvm.memory.heap.usage\":{\"value\":0.03916456777568639},\"jvm.memory.heap.used\":{\"value\":117698096},\"jvm.memory.non-heap.committed\":{\"value\":166445056},\"jvm.memory.non-heap.init\":{\"value\":7667712},\"jvm.memory.non-heap.max\":{\"value\":994050048},\"jvm.memory.non-heap.usage\":{\"value\":0.1523920694986979},\"jvm.memory.non-heap.used\":{\"value\":151485344},\"jvm.memory.pools.CodeHeap-'non-nmethods'.committed\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.max\":{\"value\":5832704},\"jvm.memory.pools.CodeHeap-'non-nmethods'.usage\":{\"value\":0.28408093398876405},\"jvm.memory.pools.CodeHeap-'non-nmethods'.used\":{\"value\":1656960},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.committed\":{\"value\":11796480},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.usage\":{\"value\":0.09536102872567315},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.used\":{\"value\":11721088},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.committed\":{\"value\":37355520},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.usage\":{\"value\":0.2538573047187417},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.used\":{\"value\":31202304},\"jvm.memory.pools.Compressed-Class-Space.committed\":{\"value\":14942208},\"jvm.memory.pools.Compressed-Class-Space.init\":{\"value\":0},\"jvm.memory.pools.Compressed-Class-Space.max\":{\"value\":367001600},\"jvm.memory.pools.Compressed-Class-Space.usage\":{\"value\":0.033858838762555805},\"jvm.memory.pools.Compressed-Class-Space.used\":{\"value\":12426248},\"jvm.memory.pools.Metaspace.committed\":{\"value\":99794944},\"jvm.memory.pools.Metaspace.init\":{\"value\":0},\"jvm.memory.pools.Metaspace.max\":{\"value\":375390208},\"jvm.memory.pools.Metaspace.usage\":{\"value\":0.25168142904782426},\"jvm.memory.pools.Metaspace.used\":{\"value\":94478744},\"jvm.memory.pools.PS-Eden-Space.committed\":{\"value\":349700096},\"jvm.memory.pools.PS-Eden-Space.init\":{\"value\":422576128},\"jvm.memory.pools.PS-Eden-Space.max\":{\"value\":1110966272},\"jvm.memory.pools.PS-Eden-Space.usage\":{\"value\":0.07505125052077188},\"jvm.memory.pools.PS-Eden-Space.used\":{\"value\":83379408},\"jvm.memory.pools.PS-Eden-Space.used-after-gc\":{\"value\":0},\"jvm.memory.pools.PS-Old-Gen.committed\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.init\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.max\":{\"value\":2253914112},\"jvm.memory.pools.PS-Old-Gen.usage\":{\"value\":0.014950035505168354},\"jvm.memory.pools.PS-Old-Gen.used\":{\"value\":33696096},\"jvm.memory.pools.PS-Old-Gen.used-after-gc\":{\"value\":23791152},\"jvm.memory.pools.PS-Survivor-Space.committed\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.init\":{\"value\":70254592},\"jvm.memory.pools.PS-Survivor-Space.max\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.usage\":{\"value\":0.59375},\"jvm.memory.pools.PS-Survivor-Space.used\":{\"value\":622592},\"jvm.memory.pools.PS-Survivor-Space.used-after-gc\":{\"value\":622592},\"jvm.memory.total.committed\":{\"value\":1644412928},\"jvm.memory.total.init\":{\"value\":1697972224},\"jvm.memory.total.max\":{\"value\":3999268864},\"jvm.memory.total.used\":{\"value\":269184904},\"jvm.thread.blocked.count\":{\"value\":0},\"jvm.thread.count\":{\"value\":82},\"jvm.thread.daemon.count\":{\"value\":11},\"jvm.thread.deadlock.count\":{\"value\":0},\"jvm.thread.deadlocks\":{\"value\":[]},\"jvm.thread.new.count\":{\"value\":0},\"jvm.thread.runnable.count\":{\"value\":25},\"jvm.thread.terminated.count\":{\"value\":0},\"jvm.thread.timed_waiting.count\":{\"value\":10},\"jvm.thread.waiting.count\":{\"value\":47}},\"counters\":{},\"histograms\":{},\"meters\":{},\"timers\":{}}\n\n$ curl -H 'Accept: application/prometheus' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n# TYPE attr_jvm_cpu_usage gauge\nattr_jvm_cpu_usage 83.0\n# TYPE attr_jvm_heap_size gauge\nattr_jvm_heap_size 1409.0\n# TYPE attr_jvm_heap_used gauge\nattr_jvm_heap_used 220.0\n# TYPE internals_0_concurrent_requests gauge\ninternals_0_concurrent_requests 1.0\n# TYPE internals_global_throttling_quotas gauge\ninternals_global_throttling_quotas 3.0\n# TYPE jvm_attr_uptime gauge\njvm_attr_uptime 2372614.0\n# TYPE jvm_gc_PS_MarkSweep_count gauge\njvm_gc_PS_MarkSweep_count 3.0\n# TYPE jvm_gc_PS_MarkSweep_time gauge\njvm_gc_PS_MarkSweep_time 261.0\n# TYPE jvm_gc_PS_Scavenge_count gauge\njvm_gc_PS_Scavenge_count 12.0\n# TYPE jvm_gc_PS_Scavenge_time gauge\njvm_gc_PS_Scavenge_time 161.0\n# TYPE jvm_memory_heap_committed gauge\njvm_memory_heap_committed 1.477967872E9\n# TYPE jvm_memory_heap_init gauge\njvm_memory_heap_init 1.690304512E9\n# TYPE jvm_memory_heap_max gauge\njvm_memory_heap_max 3.005218816E9\n# TYPE jvm_memory_heap_usage gauge\njvm_memory_heap_usage 0.07680553268571043\n# TYPE jvm_memory_heap_used gauge\njvm_memory_heap_used 2.30817432E8\n# TYPE jvm_memory_non_heap_committed gauge\njvm_memory_non_heap_committed 1.66510592E8\n# TYPE jvm_memory_non_heap_init gauge\njvm_memory_non_heap_init 7667712.0\n# TYPE jvm_memory_non_heap_max gauge\njvm_memory_non_heap_max 9.94050048E8\n# TYPE jvm_memory_non_heap_usage gauge\njvm_memory_non_heap_usage 0.15262878997416435\n# TYPE jvm_memory_non_heap_used gauge\njvm_memory_non_heap_used 1.51720656E8\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_nmethods__committed 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_nmethods__max 5832704.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_nmethods__usage 0.28408093398876405\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_nmethods__used 1656960.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__committed 1.1862016E7\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__usage 0.09610562183417755\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__used 1.1812608E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__committed 3.735552E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__usage 0.25493618368435084\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__used 3.1334912E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_committed gauge\njvm_memory_pools_Compressed_Class_Space_committed 1.4942208E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_init gauge\njvm_memory_pools_Compressed_Class_Space_init 0.0\n# TYPE jvm_memory_pools_Compressed_Class_Space_max gauge\njvm_memory_pools_Compressed_Class_Space_max 3.670016E8\n# TYPE jvm_memory_pools_Compressed_Class_Space_usage gauge\njvm_memory_pools_Compressed_Class_Space_usage 0.03386023385184152\n# TYPE jvm_memory_pools_Compressed_Class_Space_used gauge\njvm_memory_pools_Compressed_Class_Space_used 1.242676E7\n# TYPE jvm_memory_pools_Metaspace_committed gauge\njvm_memory_pools_Metaspace_committed 9.9794944E7\n# TYPE jvm_memory_pools_Metaspace_init gauge\njvm_memory_pools_Metaspace_init 0.0\n# TYPE jvm_memory_pools_Metaspace_max gauge\njvm_memory_pools_Metaspace_max 3.75390208E8\n# TYPE jvm_memory_pools_Metaspace_usage gauge\njvm_memory_pools_Metaspace_usage 0.25170985813247426\n# TYPE jvm_memory_pools_Metaspace_used gauge\njvm_memory_pools_Metaspace_used 9.4489416E7\n# TYPE jvm_memory_pools_PS_Eden_Space_committed gauge\njvm_memory_pools_PS_Eden_Space_committed 3.49700096E8\n# TYPE jvm_memory_pools_PS_Eden_Space_init gauge\njvm_memory_pools_PS_Eden_Space_init 4.22576128E8\n# TYPE jvm_memory_pools_PS_Eden_Space_max gauge\njvm_memory_pools_PS_Eden_Space_max 1.110966272E9\n# TYPE jvm_memory_pools_PS_Eden_Space_usage gauge\njvm_memory_pools_PS_Eden_Space_usage 0.17698545577448457\n# TYPE jvm_memory_pools_PS_Eden_Space_used gauge\njvm_memory_pools_PS_Eden_Space_used 1.96624872E8\n# TYPE jvm_memory_pools_PS_Eden_Space_used_after_gc gauge\njvm_memory_pools_PS_Eden_Space_used_after_gc 0.0\n# TYPE jvm_memory_pools_PS_Old_Gen_committed gauge\njvm_memory_pools_PS_Old_Gen_committed 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_init gauge\njvm_memory_pools_PS_Old_Gen_init 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_max gauge\njvm_memory_pools_PS_Old_Gen_max 2.253914112E9\n# TYPE jvm_memory_pools_PS_Old_Gen_usage gauge\njvm_memory_pools_PS_Old_Gen_usage 0.014950035505168354\n# TYPE jvm_memory_pools_PS_Old_Gen_used gauge\njvm_memory_pools_PS_Old_Gen_used 3.3696096E7\n# TYPE jvm_memory_pools_PS_Old_Gen_used_after_gc gauge\njvm_memory_pools_PS_Old_Gen_used_after_gc 2.3791152E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_committed gauge\njvm_memory_pools_PS_Survivor_Space_committed 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_init gauge\njvm_memory_pools_PS_Survivor_Space_init 7.0254592E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_max gauge\njvm_memory_pools_PS_Survivor_Space_max 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_usage gauge\njvm_memory_pools_PS_Survivor_Space_usage 0.59375\n# TYPE jvm_memory_pools_PS_Survivor_Space_used gauge\njvm_memory_pools_PS_Survivor_Space_used 622592.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_used_after_gc gauge\njvm_memory_pools_PS_Survivor_Space_used_after_gc 622592.0\n# TYPE jvm_memory_total_committed gauge\njvm_memory_total_committed 1.644478464E9\n# TYPE jvm_memory_total_init gauge\njvm_memory_total_init 1.697972224E9\n# TYPE jvm_memory_total_max gauge\njvm_memory_total_max 3.999268864E9\n# TYPE jvm_memory_total_used gauge\njvm_memory_total_used 3.82665128E8\n# TYPE jvm_thread_blocked_count gauge\njvm_thread_blocked_count 0.0\n# TYPE jvm_thread_count gauge\njvm_thread_count 82.0\n# TYPE jvm_thread_daemon_count gauge\njvm_thread_daemon_count 11.0\n# TYPE jvm_thread_deadlock_count gauge\njvm_thread_deadlock_count 0.0\n# TYPE jvm_thread_new_count gauge\njvm_thread_new_count 0.0\n# TYPE jvm_thread_runnable_count gauge\njvm_thread_runnable_count 25.0\n# TYPE jvm_thread_terminated_count gauge\njvm_thread_terminated_count 0.0\n# TYPE jvm_thread_timed_waiting_count gauge\njvm_thread_timed_waiting_count 10.0\n# TYPE jvm_thread_waiting_count gauge\njvm_thread_waiting_count 47.0\n```"},{"name":"netty-server.md","id":"/topics/netty-server.md","url":"/topics/netty-server.html","title":"Alternative HTTP server","content":"# Alternative HTTP server\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nwith the change of licence in Akka, we are experimenting around using Netty as http server for otoroshi (and getting rid of akka http)\n\nin `v1.5.14` we are introducing a new alternative http server base on [`reactor-netty`](https://projectreactor.io/docs/netty/release/reference/index.html). It also include a preview of an HTTP3 server using [netty-incubator-codec-quic](https://github.com/netty/netty-incubator-codec-quic) and [netty-incubator-codec-http3](https://github.com/netty/netty-incubator-codec-http3)\n\n## The specs\n\nthis new server can start during otoroshi boot sequence and accept HTTP/1.1 (with and without TLS), H2C and H2 (with and without TLS) connections and supporting both standard HTTP calls and websockets calls.\n\n## Enable the server\n\nto enable the server, just turn on the following flag\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\n```\n\nnow you should see something like the following in the logs\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Server options\n\nyou can also setup the host and ports of the server using\n\n```conf\notoroshi.next.experimental.netty-server.host = \"0.0.0.0\"\notoroshi.next.experimental.netty-server.http-port = 10049\notoroshi.next.experimental.netty-server.https-port = 10048\n```\n\nyou can also enable access logs using\n\n```conf\notoroshi.next.experimental.netty-server.accesslog = true\n```\n\nand enable wiretaping using \n\n```conf\notoroshi.next.experimental.netty-server.wiretap = true\n```\n\nyou can also custom number of worker thread using\n\n```conf\notoroshi.next.experimental.netty-server.thread = 0 # system automatically assign the right number of threads\n```\n\n## HTTP2\n\nyou can enable or disable HTTP2 with\n\n```conf\notoroshi.next.experimental.netty-server.http2.enabled = true\notoroshi.next.experimental.netty-server.http2.h2c = true\n```\n\n## HTTP3\n\nyou can enable or disable HTTP3 (preview ;) ) with\n\n```conf\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048 # yep can the the same as https because its on the UDP stack\n```\n\nthe result will be something like\n\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Native transport\n\nIt is possible to enable native transport for the server\n\n```conf\notoroshi.next.experimental.netty-server.native.enabled = true\notoroshi.next.experimental.netty-server.native.driver = \"Auto\"\n```\n\npossible values for `otoroshi.next.experimental.netty-server.native.driver` are \n\n- `Auto`: the server try to find the best native option available\n- `Epoll`: the server uses Epoll native transport for Linux environments\n- `KQueue`: the server uses KQueue native transport for MacOS environments\n- `IOUring`: the server uses IOUring native transport for Linux environments that supports it (experimental, using [netty-incubator-transport-io_uring](https://github.com/netty/netty-incubator-transport-io_uring))\n\nthe result will be something like when starting on a Mac\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - using KQueue native transport\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Env. variables\n\nyou can configure the server using the following env. variables\n\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER`\n\n"},{"name":"opentelemetry.md","id":"/topics/opentelemetry.md","url":"/topics/opentelemetry.html","title":"OpenTelemetry support","content":"# OpenTelemetry support\n\nOpenTelemetry is an open-source project focused on providing a set of APIs, libraries, agents, and instrumentation to \nenable observability in modern software applications. It helps developers and software teams collect, process, \nand export telemetry data, which includes metrics, traces, and logs, from their applications and infrastructure. \nThe project aims to provide a standardized approach to instrumenting applications for distributed tracing, metrics, and logging.\n\nHere's a breakdown of the key components of OpenTelemetry:\n\n- **Tracing**: Distributed tracing is a method used to monitor and understand the flow of requests across different services \nin a distributed system. OpenTelemetry allows developers to add instrumentation to their code to trace requests as they \nflow through various services, providing insights into performance bottlenecks and dependencies between components.\n- **Metrics**: Metrics are quantitative measurements that provide information about the behavior and performance of \nan application. OpenTelemetry enables developers to collect metrics from their applications, such as CPU usage, memory \nconsumption, and custom application-specific metrics, to gain visibility into the application's health and performance.\n- **Logging**: OpenTelemetry also supports capturing and exporting logs, which are textual records of events and messages \nthat occur during the execution of an application. Logs are essential for debugging and monitoring purposes, and \nOpenTelemetry allows developers to integrate logging with other telemetry data, making it easier to correlate events.\n\nOpenTelemetry is designed to be language-agnostic and vendor-agnostic, supporting multiple programming languages and \nvarious telemetry backends. This flexibility makes it easier for developers to adopt the OpenTelemetry standard \nregardless of their technology stack.\n\nThe goal of OpenTelemetry is to promote a consistent way of collecting telemetry data across different applications \nand environments, making it easier for developers to adopt observability best practices. By leveraging OpenTelemetry, \nsoftware teams can gain deeper insights into the behavior of their systems and improve performance, troubleshoot \nissues, and enhance the overall reliability of their applications.\n\nNow, OpenTelemetry is officialy supported in Otoroshi and can be used in different parts of your instance. You can use \nit to collect otoroshi server logs and otoroshi server metrics through config. file. Then you have access to 2 new data \nexporter that can export otoroshi events to OpenTelemetry log collector and send custom metrics to an OpenTelemetry metrics collector.\n\n## server logs\n\notoroshi server logs can be sent to an OpenTelemetry log collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-logs.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the logs push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-logs {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## server metrics\n\notoroshi server metrics can be sent to an OpenTelemetry metrics collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-metrics.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the metrics push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-metrics {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## logs data expoter\n\nA new kind of data exporter is now available to send otoroshi events serialized as text to an OpenTelemetry log collector. \nFirst create a new data exporter and select the type `otlp-logs`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\n@@@ div { .centered-img }\n\n@@@\n\n## metrics data exporter\n\nA new kind of data exporter is now available to send custom metrics derived from otoroshi events to an OpenTelemetry metrics collector. \nFirst create a new data exporter and select the type `otlp-metrics`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\nThen you will be able to add new metrics on this data exporter with a name, the type of metric (counter, timer, histogram), the value and the kind of event it's based on.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n\n\n"},{"name":"otoroshi-protocol.md","id":"/topics/otoroshi-protocol.md","url":"/topics/otoroshi-protocol.html","title":"The Otoroshi communication protocol","content":"# The Otoroshi communication protocol\n\nThe exchange protocol secure the communication with an app. When it's enabled, Otoroshi will send for each request a value in pre-selected token header, and will check the same header in the return request. On routes, you will have to use the `Otoroshi challenge token` plugin to enable it.\n\n### V1 challenge\n\nIf you enable secure communication for a given service with `V1 - simple values exchange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header and return it in a header named `Otoroshi-State-Resp`. \n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V1 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### V2 challenge\n\nIf you enable secure communication for a given service with `V2 - signed JWT token exhange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header value containing a JWT token, verify it's content signature then extract a claim named `state` and return a new JWT token in a header named `Otoroshi-State-Resp` with the `state` value in a claim named `state-resp`. By default, the signature algorithm is HMAC+SHA512 but can you can choose your own. The sent and returned JWT tokens have short TTL to avoid being replayed. You must be validate the tokens TTL. The audience of the response token must be `Otoroshi` and you have to specify `iat`, `nbf` and `exp`.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V2 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### Info. token\n\nOtoroshi is also sending a JWT token in a header named `Otoroshi-Claim` that the target app can validate too. On routes, you will have to use the `Otoroshi info. token` plugin to enable it.\n\nThe `Otoroshi-Claim` is a JWT token containing some informations about the service that is called and the client if available. You can choose between a legacy version of the token and a new one that is more clear and structured.\n\nBy default, the otoroshi jwt token is signed with the `otoroshi.claim.sharedKey` config property (or using the `$CLAIM_SHAREDKEY` env. variable) and uses the `HMAC512` signing algorythm. But it is possible to customize how the token is signed from the service descriptor page in the `Otoroshi exchange protocol` section. \n\n@@@ div { .centered-img }\n\n@@@\n\nusing another signing algo.\n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can choose the signing algorithm and the secret/keys used. You can use syntax like `${env.MY_ENV_VAR}` or `${config.my.config.path}` to provide secret/keys values. \n\nFor example, for a service named `my-service` with a signing key `secret` with `HMAC512` signing algorythm, the basic JWT token that will be sent should look like the following\n\n```\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiItLSIsImF1ZCI6Im15LXNlcnZpY2UiLCJpc3MiOiJPdG9yb3NoaSIsImV4cCI6MTUyMTQ0OTkwNiwiaWF0IjoxNTIxNDQ5ODc2LCJqdGkiOiI3MTAyNWNjMTktMmFjNy00Yjk3LTljYzctMWM0ODEzYmM1OTI0In0.mRcfuFVFPLUV1FWHyL6rLHIJIu0KEpBkKQCk5xh-_cBt9cb6uD6enynDU0H1X2VpW5-bFxWCy4U4V78CbAQv4g\n```\n\nif you decode it, the payload will look something like\n\n```json\n{\n \"sub\": \"apikey_client_id\",\n \"aud\": \"my-service\",\n \"iss\": \"Otoroshi\",\n \"exp\": 1521449906,\n \"iat\": 1521449876,\n \"jti\": \"71025cc19-2ac7-4b97-9cc7-1c4813bc5924\"\n}\n```\n\nIf you want to validate the `Otoroshi-Claim` on the target app side to ensure that the input requests only comes from `Otoroshi`, you will have to write an HTTP filter to do the job. For instance, if you want to write a filter to make sure that requests only comes from Otoroshi, you can write something like the following (using playframework 2.6).\n\nScala\n: @@snip [filter.scala](../snippets/filter.scala)\n\nJava\n: @@snip [filter.java](../snippets/filter.java)\n"},{"name":"pki.md","id":"/topics/pki.md","url":"/topics/pki.html","title":"Otoroshi's PKI","content":"# Otoroshi's PKI\n\nWith Otoroshi, you can add your own certificates, your own CA and even create self signed certificates or certificates from CAs. You can enable auto renewal of thoses self signed certificates or certificates generated. Certificates have to be created with the certificate chain and the private key in PEM format.\n\nAn Otoroshi instance always starts with 5 auto-generated certificates. \n\nThe highest certificate is the **Otoroshi Default Root CA Certificate**. This certificate is used by Otoroshi to sign the intermediate CA.\n\n**Otoroshi Default Intermediate CA Certificate**: first intermediate CA that must be used to issue new certificates in Otoroshi. Creating certificates directly from the CA root certificate increases the risk of root certificate compromise, and if the CA root certificate is compromised, the entire trust infrastructure built by the SSL provider will fail\n\nThis intermediate CA signed three certificates :\n\n* **Otoroshi Default Client certificate**: \n* **Otoroshi Default Jwt Signing Keypair**: default keypair (composed of a public and private key), exposed on `https://xxxxxx/.well-known/jwks.json`, that can be used to sign and verify JWT verifier\n* **Otoroshi Default Wildcard Certificate**: this certificate has `*.oto.tools` as common name. It can be very useful to the development phase\n\n## The PKI API\n\nThe Otoroshi's PKI can be managed using the admin api of otoroshi (by default admin api is exposed on https://otoroshi-api.xxxxx)\n\nLink to the complete swagger section about PKI : https://maif.github.io/otoroshi/swagger-ui/index.html#/pki\n\n* `POST` [/api/pki/certs/_letencrypt](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genLetsEncryptCert): generates a certificate using Let's Encrypt or any ACME compatible system\n* `POST` [/api/pki/certs/_p12](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.importCertFromP12): import a .p12 file as client certificates\n* `POST` [/api/pki/certs/_valid](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateIsValid): check if a certificate is valid (based on its own data)\n* `POST` [/api/pki/certs/_data](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateData): extract data from a certificate\n* `POST` [/api/pki/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCert): generates a self signed certificates\n* `POST` [/api/pki/csrs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCsr) : generates a CSR\n* `POST` [/api/pki/keys](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genKeyPair) : generates a keypair\n* `POST` [/api/pki/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCA) : generates a self signed CA\n* `POST` [/api/pki/cas/:ca/certs/_sign](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.signCert): sign a certificate based on CSR\n* `POST` [/api/pki/cas/:ca/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCert): generates a certificate\n* `POST` [/api/pki/cas/:ca/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSubCA) : generates a sub-CA\n\n## The PKI UI\n\nAll generated certificates are listed in the `https://xxxxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe PKI UI are composed of these following actions:\n\n* **Add item**: redirects the user on the certificate creation page. It’s useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* **Let's Encrypt certificate**: asks a certificate matching a given host to Let’s encrypt\n* **Create certificate**: issues a certificate with an existing Otoroshi certificate as CA. You can create a client certificate, a server certificate or a keypair certiciate that will be used to verify and sign JWT tokens.\n* **Import .p12 file**: loads a p12 file as certificate\n\nUnder these buttons, you have the list of current certificates, imported or generated, revoked or not. For each certificate, you will find: \n\n* a **name** \n* a **description** \n* the **subject** \n* the **type** of certificate (CA / client / keypair / certificate)\n* the **revoked reason** (empty if not) \n* the **creation date** following by its **expiration date**.\n\n## Exposed public keys\n\nThe Otoroshi certificate can be turned and used as keypair (simple action that can be executed by editing a certificate or during its creation, or using the admin api). A Otoroski keypair can be used to sign and verify JWT tokens with asymetric signature. Once a jwt token is signed with a keypair, it can be necessary to provide a way to the services to verify the tokens received by Otoroshi. This usage is cover by Otoroshi by the flag `Public key exposed`, available on each certificate.\n\nOtoroshi exposes each keypair with the flag enabled, on the following routes:\n\n* `https://xxxxxxxxx.xxxxxxx.xx/.well-known/otoroshi/security/jwks.json`\n* `https://otoroshi-api.xxxxxxx.xx/.well-known/jwks.json`\n\nOn these routes, you will find the list of public keys exposed using [the JWK standard](https://datatracker.ietf.org/doc/html/rfc7517)\n\n\n## OCSP Responder\n\nOtoroshi is able to revocate a certificate, directly from the UI, and to add a revocation status to specifiy the reason. The revocation reason can be :\n\n* `VALID`: The certificate is not revoked\n* `UNSPECIFIED`: Can be used to revoke certificates for reasons other than the specific codes.\n* `KEY_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `CA_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `AFFILIATION_CHANGED`: The subject's name or other information in the certificate has been modified but there is no cause to suspect that the private key has been compromised.\n* `SUPERSEDED`: The certificate has been superseded but there is no cause to suspect that the private key has been compromised\n* `CESSATION_OF_OPERATION`: The certificate is no longer needed for the purpose for which it was issued but there is no cause to suspect that the private key has been compromised\n* `CERTIFICATE_HOLD`: The certificate is temporarily revoked but there is no cause to suspect that the private kye has been compromised\n* `REMOVE_FROM_CRL`: The certificate has been unrevoked\n* `PRIVILEGE_WITH_DRAWN`: The certificate was revoked because a privilege contained within that certificate has been withdrawn\n* `AA_COMPROMISE`: It is known or suspected that aspects of the AA validated in the attribute certificate, have been compromised\n\nOtoroshi supports the Online Certificate Status Protocol for obtaining the revocation status of its certificates. The OCSP endpoint is also add to any generated certificate. This endpoint is available at `https://otoroshi-api.xxxxxx/.well-known/otoroshi/security/ocsp`\n\n## A.I.A : Authority Information Access\n\nOtoroshi provides a way to add the A.I.A in the certificate. This certificate extension contains :\n\n* Information about how to get the issuer of this certificate (CA issuer access method)\n* Address of the OCSP responder from where revocation of this certificate can be checked (OCSP access method)\n\n`https://xxxxxxxxxx/.well-known/otoroshi/security/certificates/:cert-id`"},{"name":"relay-routing.md","id":"/topics/relay-routing.md","url":"/topics/relay-routing.html","title":"Relay Routing","content":"# Relay Routing\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nRelay routing is the capability to forward traffic between otoroshi leader nodes based on network location of the target. Let say we have an otoroshi cluster split accross 3 network zones. Each zone has \n\n- one or more datastore instances\n- one or more otoroshi leader instances\n- one or more otoroshi worker instances\n\nthe datastores are replicated accross network zones in an active-active fashion. Each network zone also have applications, apis, etc deployed. Sometimes the same application is deployed in multiple zones, sometimes not. \n\nit can quickly become a nightmare when you want to access an application deployed in one network zone from another network zone. You'll have to publicly expose this application to be able to access it from the other zone. This pattern is fine, but sometimes it's not enough. With `relay routing`, you will be able to flag your routes as being deployed in one zone or another, and let otoroshi handle all the heavy lifting to route the traffic to the right network zone for you.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through relay nodes\n@@@\n\n## Otoroshi instance setup\n\nfirst of all, for every otoroshi instance deployed, you have to flag where the instance is deployed and, for leaders, how this instance can be contacted from other zones (this is a **MAJOR** requirement, without that, you won't be able to make relay routing work). Also, you'll have to enable the @ref:[new proxy engine](./engine.md).\n\nIn the otoroshi configuration file, for each instance, enable relay routing and configure where the instance is located and how the leader can be contacted\n\n```conf\notoroshi {\n ...\n cluster {\n mode = \"leader\" # or \"worker\" dependending on the instance kind\n ...\n relay {\n enabled = true # enable relay routing\n leaderOnly = true # use leaders as the only kind of relay node\n location { # you can use all those parameters at the same time. There is no actual network concepts bound here, just some kind of tagging system, so you can use it as you wish\n provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER}\n zone = \"zone-1\"\n region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION}\n datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER}\n rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK}\n }\n exposition {\n urls = [\"https://otoroshi-api-zone-1.my.domain:443\"]\n hostname = \"otoroshi-api-zone-1.my.domain\"\n clientId = \"apkid_relay-routing-apikey\"\n }\n }\n }\n}\n```\n\nalso, to make your leaders exposed by zone, do not hesitate to add domain names to the `otoroshi-admin-api` service and setup your DNS to bind those domains to the right place\n\n@@@ div { .centered-img }\n\n@@@\n\n## Route setup for an application deployed in only one zone\n\nNow, for any route/service deployed in only one zone, you will be able to flag it using its metadata as being deployed in one zone or another. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1`\n\n## Route setup for an application deployed in multiple zones at the same time\n\nNow, for any route/service deployed in multiple zones zones at the same time, you will be able to flag it using its metadata as being deployed in some zones. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1, zone-2` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1` or `zone-2`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1` or `zone-2` and load balance between them.\n\nalso, you will have to setup your targets to avoid trying to contact targets that are not actually in the current zone. To do that, you'll have to set the target predicate to `NetworkLocationMatch` and fill the possible locations according to the actual location of your target\n\n@@@ div { .centered-img }\n\n@@@\n\n## Demo\n\nyou can find a demo of this setup [here](https://github.com/MAIF/otoroshi/tree/master/demos/relay). This is a `docker-compose` setup with multiple network to simulate network zones. You also have an otoroshi export to understand how to setup your routes/services\n"},{"name":"secrets.md","id":"/topics/secrets.md","url":"/topics/secrets.html","title":"Secrets management","content":"# Secrets management\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSecrets are generally confidential values that should not appear in plain text in the application. There are several products that help you store, retrieve, and rotate these secrets securely. Otoroshi offers a mechanism to set up references to these secrets in its entities to benefits from the perks of your existing secrets management infrastructure. This feature only work with the @ref:[new proxy engine](./engine.md).\n\nA secret can be anything you want like an apikey secret, a certificate private key or password, a jwt verifier signing key, a password to a proxy, a value for a header, etc.\n\n## Enable secrets management in otoroshi\n\nBy default secrets management is disbaled. You can enable it by setting `otoroshi.vaults.enabled` or `${OTOROSHI_VAULTS_ENABLED}` to `true`.\n\n## Global configuration\n\nSecrets management can be only configured using otoroshi static configuration file (also using jvm args mechanism). \nThe configuration is located at `otoroshi.vaults` where you can find the global configuration of the secrets management system and the configurations for each enabled secrets management backends. Basically it looks like\n\n```conf\nvaults {\n enabled = false\n enabled = ${?OTOROSHI_VAULTS_ENABLED}\n secrets-ttl = 300000 # 5 minutes\n secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL}\n cached-secrets = 10000\n cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS}\n read-timeout = 10000 # 10 seconds\n read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT}\n # if enabled, only leader nodes fetches the secrets.\n # entities with secret values filled are then sent to workers when they poll the cluster state.\n # only works if `otoroshi.cluster.autoUpdateState=true`\n leader-fetch-only = false\n leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY}\n env {\n type = \"env\"\n prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX}\n }\n}\n```\n\nyou can see here the global configuration and a default backend configured that can retrieve secrets from environment variables. \n\nThe configuration keys can be used for \n\n- `secrets-ttl`: the amount of milliseconds before the secret value is read again from backend\n- `cached-secrets`: the number of secrets that will be cached on an otoroshi instance\n- `read-timeout`: the timeout (in milliseconds) to read a secret from a backend\n\n## Entities with secrets management\n\nthe entities that support secrets management are the following \n\n- `routes`\n- `services`\n- `service_descriptors`\n- `apikeys`\n- `certificates`\n- `jwt_verifiers`\n- `authentication_modules`\n- `targets`\n- `backends`\n- `tcp_services`\n- `data_exporters`\n\n## Define a reference to a secret\n\nin the previously listed entities, you can define, almost everywhere, references to a secret using the following syntax:\n\n`${vault://name_of_the_vault/secret/of/the/path}`\n\nlet say I define a new apikey with the following value as secret `${vault://my_env/apikey_secret}` with the following secrets management configuration\n\n```conf\nvaults {\n enabled = true\n secrets-ttl = 300000\n cached-secrets = 10000\n read-ttl = 10000\n my_env {\n type = \"env\"\n }\n}\n```\n\nif the machine running otoroshi has an environment variable named `APIKEY_SECRET` with the value `verysecret`, then you will be able to can an api with the defined apikey `client_id` and a `client_secret` value of `verysecret`\n\n```sh\ncurl 'http://my-awesome-api.oto.tools:8080/api/stuff' -u awesome_apikey:verysecret\n```\n\n## Possible backends\n\nOtoroshi comes with the support of several secrets management backends.\n\n### Environment variables\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"env\"\n prefix = \"the_prefix_added_to_the_name_of_the_env_variable\"\n }\n}\n```\n\n### Local\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"local\"\n root = \"the_root_path/in_otoroshi/environment\"\n }\n}\n```\n\nvalue of this vault can be configured in the danger zone > Global metadata > Otoroshi environment.\n\n### Infisical\n\na backend for the awesome open source project [Infisical](https://infisical.com/). It support both E2EE and non E2EE secrets.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"infisical\"\n baseUrl = \"https://app.infisical.com\" # optional, the base url of your infisical server, fallbacks to https://app.infisical.com\n serviceToken = \"st.xxxx.yyyy.zzzz\" # the service token for your projet\n e2ee = true # are you secrets end to end encrypted\n defaultSecretType = \"shared\" # optional, fallbacks to shared\n defaultWorkspaceId = \"xxxxxx\" # optional, value can be passed in the secret address\n defaultEnvironment = \"dev\" # optional, value can be passed in the secret address\n }\n}\n```\n\nyou should define your references like `${vault://infisical_vault/my_secret_path?workspaceId=xxx&environment=dev&type=shared}`. `workspaceId`, `environment` and `type` are optional if filled in global config. \n\nYou can also pass a `json_pointer=/foo/bar` to handle the value like a json document a select a value inside it.\n\n### Hashicorp Vault\n\na backend for [Hashicorp Vault](https://www.vaultproject.io/). Right now we only support KV engines.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"hashicorp-vault\"\n url = \"http://127.0.0.1:8200\"\n mount = \"kv\" # the name of the secret store in vault\n kv = \"v2\" # the version of the kv store (v1 or v2)\n token = \"root\" # the token that can access to your secrets\n }\n}\n```\n\nyou should define your references like `${vault://hashicorp_vault/secret/path/key_name}`.\n\n\n### Azure Key Vault\n\na backend for [Azure Key Vault](https://azure.microsoft.com/en-en/services/key-vault/). Right now we only support secrets and not keys and certificates.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"azure\"\n url = \"https://keyvaultname.vault.azure.net\"\n api-version = \"7.2\" # the api version of the vault\n tenant = \"xxxx-xxx-xxx\" # your azure tenant id, optional\n client_id = \"xxxxx\" # your azure client_id\n client_secret = \"xxxxx\" # your azure client_secret\n # token = \"xxx\" possible if you have a long lived existing token. will take over tenant / client_id / client_secret\n }\n}\n```\n\nyou should define your references like `${vault://azure_vault/secret_name/secret_version}`. `secret_version` is mandatory\n\nIf you want to use certificates and keys objects from the azure key vault, you will have to specify an option in the reference named `azure_secret_kind` with possible value `certificate`, `privkey`, `pubkey` like the following :\n\n```\n${vault://azure_vault/myprivatekey/secret_version?azure_secret_kind=privkey}\n```\n\n### AWS Secrets Manager\n\na backend for [AWS Secrets Manager](https://aws.amazon.com/en/secrets-manager/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"aws\"\n access-key = \"key\"\n access-key-secret = \"secret\"\n region = \"eu-west-3\" # the aws region of your secrets management\n }\n}\n```\n\nyou should define your references like `${vault://aws_vault/secret_name/secret_version}`. `secret_version` is optional\n\n### Google Cloud Secrets Manager\n\na backend for [Google Cloud Secrets Manager](https://cloud.google.com/secret-manager)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"gcloud\"\n url = \"https://secretmanager.googleapis.com\"\n apikey = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://gcloud_vault/projects/foo/secrets/bar/versions/the_version}`. `the_version` can be `latest`\n\n### AlibabaCloud Cloud Secrets Manager\n\na backend for [AlibabaCloud Secrets Manager](https://www.alibabacloud.com/help/en/doc-detail/152001.html)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"alibaba-cloud\"\n url = \"https://kms.eu-central-1.aliyuncs.com\"\n access-key-id = \"access-key\"\n access-key-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://alibaba_vault/secret_name}`\n\n\n### Kubernetes Secrets\n\na backend for [Kubernetes secrets](https://kubernetes.io/en/docs/concepts/configuration/secret/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"kubernetes\"\n # see the configuration of the kubernetes plugin, \n # by default if the pod if well configured, \n # you don't have to setup anything\n }\n}\n```\n\nyou should define your references like `${vault://k8s_vault/namespace/secret_name/key_name}`. `key_name` is optional. if present, otoroshi will try to lookup `key_name` in the secrets `stringData`, if not defined the secrets `data` will be base64 decoded and used.\n\n\n### Izanami config.\n\na backend for [Izanami config.](https://maif.github.io/izanami/manual/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"izanami\"\n url = \"http://127.0.0.1:8200\"\n client-id = \"client\"\n client-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://izanami_vault/the:secret:id/key_name}`. `key_name` is optional if the secret value is not a json object\n\n### Spring Cloud Config\n\na backend for [Spring Cloud Config.](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"spring-cloud\"\n url = \"http://127.0.0.1:8000\"\n root = \"myapp/prod\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://spring_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n\n### Http backend\n\na backend for that uses the result of an http endpoint\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"http\"\n url = \"http://127.0.0.1:8000/endpoint/for/config\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://http_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n"},{"name":"sessions-mgmt.md","id":"/topics/sessions-mgmt.md","url":"/topics/sessions-mgmt.html","title":"Sessions management","content":"# Sessions management\n\n## Admins\n\nAll logged users to an Otoroshi instance are administrators. An user session is created for each sucessfull connection to the UI. \n\nThese sessions are listed in the `Admin users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/admin`).\n\nAn admin user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Rights`: list of rules to authorize the connected user on each tenant and teams.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n\nIn the `Admin users sessions` page, you have two more actions:\n\n* `Discard all sessions`: kills all current sessions (including the session of the owner of this action)\n* `Discard old sessions`: kill all outdated sessions\n\n## Private apps\n\nAll logged users to a protected application has an private user session.\n\nThese sessions are listed in the `Private apps users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/private`).\n\nAn private user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Meta.`: list of metadatas added by the authentication module.\n* `Tokens`: list of tokens received from the identity provider used. In the case of a memory authentication, this part will keep empty.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n"},{"name":"tls.md","id":"/topics/tls.md","url":"/topics/tls.html","title":"TLS","content":"# TLS\n\nas you might have understand, otoroshi can store TLS certificates and use them dynamically. It means that once a certificate is imported or created in otoroshi, you can immediately use it to serve http request over TLS, to call https backends that requires mTLS or that do not have certicates signed by a globally knowned authority.\n\n## TLS termination\n\nany certficate added to otoroshi with a valid `CN` and `SANs` can be used in the following seconds to serve https requests. If you do not provide a private key with a certificate chain, the certificate will only be trusted like a CA. If you want to perform mTLS calls on you otoroshi instance, do not forget to enabled it (it is disabled by default for performance reasons as the TLS handshake is bigger with mTLS enabled)\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\n### TLS termination configuration\n\nYou can configure TLS termination statically using config. file or env. variables. Everything is available at `otoroshi.tls`\n\n```conf\notoroshi {\n tls {\n # the cipher suites used by otoroshi TLS termination\n cipherSuitesJDK11 = [\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuitesJDK8 = [\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_RSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuites = []\n # the protocols used by otoroshi TLS termination\n protocolsJDK11 = [\"TLSv1.3\", \"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"]\n protocolsJDK8 = [\"SSLv2Hello\", \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n protocols = []\n # the JDK cacert access\n cacert {\n path = \"$JAVA_HOME/lib/security/cacerts\"\n password = \"changeit\"\n }\n # the mtls mode\n fromOutside {\n clientAuth = \"None\"\n clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH}\n }\n # the default trust mode\n trust {\n all = false\n all = ${?OTOROSHI_SSL_TRUST_ALL}\n }\n # some initial cacert access, useful to include non standard CA when starting (file paths)\n initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT}\n initialCacert = ${?INITIAL_CACERT}\n initialCert = ${?CLUSTER_WORKER_INITIAL_CERT}\n initialCert = ${?INITIAL_CERT}\n initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY}\n initialCertKey = ${?INITIAL_CERT_KEY}\n # initialCerts = [] \n }\n}\n```\n\n\n### TLS termination settings\n\nIt is possible to adjust the behavior of the TLS termination from the `danger zone` at the `Tls Settings` section. Here you can either define that a non-matching SNI call will use a random TLS certtificate to reply or will use a default domain (the TLS certificate associated to this domain) to reply. Here you can also choose if you want to trust all the CAs trusted by your JDK when performing TLS calls `Trust JDK CAs (client)` or when receiving mTLS calls `Trust JDK CAs (server)`. If you disable the later, it is possible to select the list of CAs presented to the client during mTLS handshake.\n\n### Certificates auto generation\n\nit is also possible to generate non-existing certificate on the fly without losing the request. If you are interested by this feature, you can enable it in the `danger zone` at the `Auto Generate Certificates` section. Here you'll have to enable it and select the CA that will generate the certificate. Of course, the client will have to trust the selected CA. You can also add filters to choose which domain are allowed to generate certificates or not. The `Reply Nicely` flag is used to reply a nice error message (ie. human readable) telling that it's not possible to have an auto certficate for the current domain. \n\n## Backends TLS and mTLS calls\n\nFor any call to a backend, it is possible to customize the TLS behavior \n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can define your level of trust (trust all, loose verification) or even select on or more CAs you will trust for the following backend calls. You can also select the client certificate that will be used for the following backend calls\n\n## Keypair for signing and verification\n\nIt is also possible to use the keypair contained in a certificate to sign and verificate JWT token signature. You can mark an existing certificate in otoroshi as a keypair using the `keypair` on the certificate page.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"tunnels.md","id":"/topics/tunnels.md","url":"/topics/tunnels.html","title":"Otoroshi tunnels","content":"# Otoroshi tunnels\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSometimes, exposing apis that lives in our private network can be a nightmare, especially from a networking point of view. \nWith otoroshi tunnels, this is now trivial, as long as your internal otoroshi (that lives inside your private network) is able to contact an external otoroshi (exposed on the internet).\n\n@@@ warning { .margin-top-20 }\nYou have to enable cluster mode (Leader or Worker) to make this feature work. As this feature is experimental, we only support simple http request right now. Server Sent Event and Websocket request are not supported at the moment.\n@@@\n\n## How Otoroshi tunnels works\n\nthe main idea behind otoroshi tunnels is that the connection between your private network et the public network is initiated by the private network side. You don't have to expose a part of your private network, create a DMZ or whatever, you just have to authorize your private network otoroshi instance to contact your public network otoroshi instance.\n\n@@@ div { .centered-img }\n\n@@@\n\nonce the persistent tunnel has been created, you can create routes on the public otoroshi instance that uses the otoroshi `Remote tunnel calls` to target your remote routes through the designated tunnel instance \n\n\n@@@ div { .centered-img }\n\n@@@\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through otoroshi tunnels\n@@@\n\n## Otoroshi tunnel example\n\nfirst you have to enable the tunnels feature in your otoroshi configuration (on both public and private instances)\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n enabled = ${?OTOROSHI_TUNNELS_ENABLED}\n ...\n }\n}\n```\n\nthen you can setup a tunnel instance on your private instance to contact your public instance\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n ...\n public-apis {\n id = \"public-apis\"\n name = \"public apis tunnel\"\n url = \"https://otoroshi-api.company.com:443\"\n host = \"otoroshi-api.company.com\"\n clientId = \"xxx\"\n clientSecret = \"xxxxxx\"\n # ipAddress = \"127.0.0.1\" # optional: ip address of the public instance admin api\n # tls { # optional: TLS settings to access the public instance admin api\n # ... \n # }\n # export-routes = true # optional: send routes information to remote otoroshi instance to facilitate remote route exposition\n # export-routes-tag = \"tunnel-exposed\" # optional: only send routes information if the route has this tag\n }\n }\n}\n```\n\nNow when your private otoroshi instance will boot, a persistent tunnel will be made between private and public instance. \nNow let say you have a private api exposed on `api-a.company.local` on your private otoroshi instance and you want to expose it on your public otoroshi instance. \n\nFirst create a new route exposed on `api-a.company.com` that targets `https://api-a.company.local:443`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen add the `Remote tunnel calls` plugin to your route and set the tunnel id to `public-apis` to match the id you set in the otoroshi config file\n\n@@@ div { .centered-img }\n\n@@@\n\nadd all the plugin you need to secure this brand new public api and call it\n\n```sh\ncurl \"https://api-a.company.com/users\" | jq\n```\n\n## Easily expose your remote services\n\nyou can see all the connected tunnel instances on an otoroshi instance on the `Connected tunnels` (`Cog icon` / `Connected tunnels`). For each tunnel instance you will be able to check the tunnel health and also to easily expose all the routes available on the other end of the tunnel. Just clic on the `expose` button of the route you want to expose, and a new route will be created with the `Remote tunnel calls` plugin already setup.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"user-rights.md","id":"/topics/user-rights.md","url":"/topics/user-rights.html","title":"Otoroshi user rights","content":"# Otoroshi user rights\n\nIn Otoroshi, all users are considered **Administrators**. This choice is reinforced by the fact that Otoroshi is designed to be an administrator user interface and not an interface for users who simply want to view information. For this type of use, we encourage to use the admin API rather than giving access to the user interface.\n\nThe Otoroshi rights are split by a list of authorizations on **organizations** and **teams**. \n\nLet's taking an example where we want to authorize an administrator user on all organizations and teams.\n\nThe list of rights will be :\n\n```json\n[\n {\n \"tenant\": \"*:rw\", # (1)\n \"teams\": [\"*:rw\"] # (2)\n }\n]\n```\n\n* (1): this field, separated by a colon, indicates the name of the tenant and the associated rights. In our case, we set `*` to apply the rights to all tenants, and the `rw` to get the read and write access on them.\n* (2): the `teams` array field, represents the list of rights, applied by team. The behaviour is the same as the tenant field, we define the team or the wildcard, followed by the rights\n\nif you want to have an user that is administrator only for one organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that is administrator only for two organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that can only see 3 teams of one organization and one team in the other, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\n \"team-1:rw\",\n \"team-2:rw\",\n \"team-3:rw\",\n ]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\n \"team-4:rw\"\n ]\n }\n]\n```\n\nThe list of possible rights for an organization or a team is:\n\n* **r**: read access\n* **w**: write access\n* **not**: none access to the resource\n\nThe list of possible tenant and teams are your created tenants and teams, and the wildcard to define rights to all resources once.\n\nThe user rights is defined by the @ref:[authentication modules](../entities/auth-modules.md).\n"},{"name":"wasm-usage.md","id":"/topics/wasm-usage.md","url":"/topics/wasm-usage.html","title":"Otoroshi and WASM","content":"# Otoroshi and WASM\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. These plugins are:\n\n- `WasmRouteMatcher`: useful to define if a route can handle a request\n- `WasmPreRoute`: useful to check request and extract useful stuff for the other plugins\n- `WasmAccessValidator`: useful to control access to a route (jump to the next section to learn more about it)\n- `WasmRequestTransformer`: transform the content of an incoming request (body, headers, etc ...)\n- `WasmBackend`: execute a WASM file as Otoroshi target. Useful to implement user defined logic and function at the edge\n- `WasmResponseTransformer`: transform the content of the response produced by the target\n- `WasmSink`: create a sink plugin to handle unmatched requests\n- `WasmRequestHandler`: create a plugin that can handle the whole request lifecycle\n- `WasmJob`: create a job backed by a wasm function\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Available tutorials\n\nhere is the list of available tutorials about wasm in Otoroshi\n\n1. @ref:[Install a Wasmo](../how-to-s/wasmo-installation.md)\n2. @ref:[Use a WASM plugin](../how-to-s/wasm-usage.md)\n\n## Wasm plugins entities\n\nOtoroshi provides a dedicated entity for wasm plugins. Those entities makes it easy to declare a wasm plugin with specific configuration only once and use it in multiple places. \n\nYou can find wasm plugin entities at `/bo/dashboard/wasm-plugins`\n\nIn a wasm plugin entity, you can define the source of your wasm plugin. You can choose between\n\n- `base64`: a base64 encoded wasm script\n- `file`: the path to a wasm script file\n- `http`: the url to a wasm script file\n- `wasmo`: the name of a wasm script compiled by a Wasmo instance\n\nthen you can define the number of memory pages available for each plugin instanciation, the name of the function you want to invoke, the config. map of the VM and if you want to keep a wasm vm alive during the request lifecycle to be able to reuse it in different plugin steps\n\n@@@ div { .centered-img }\n\n@@@\n\n## Otoroshi plugins api\n\nthe following parts illustrates the apis for the different plugins. Otoroshi uses [Extism](https://extism.org/) to handle content sharing between the JVM and the wasm VM. All structures are sent to/from the plugins as json strings. \n\nfor instance, if we want to write a `WasmBackendCall` plugin using javascript, we could write something like\n\n```js\nfunction backend_call() {\n const input_str = Host.inputString(); // here we get the context passed by otoroshi as json string\n const backend_call_context = JSON.parse(input_str); // and parse it\n if (backend_call_context.path === '/hello') {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n message: `Hello ${ctx.request.query.name[0]}!` \n },\n status: 200,\n }));\n } else {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n error: \"not found\"\n },\n status: 404,\n }));\n }\n return 0; // we return 0 to tell otoroshi that everything went fine\n}\n```\n\nthe following examples are written in rust. the rust macros provided by extism makes the usage of `Host.inputString` and `Host.outputString` useless. Remember that it's still used under the hood and that the structures are passed as json strings.\n\ndo not forget to add the extism pdk library to your project to make it compile\n\nCargo.toml\n: @@snip [Cargo.toml](../snippets/wasmo/Cargo.toml) \n\ngo.mod\n: @@snip [go.mod](../snippets/wasmo/go.mod) \n\npackage.json\n: @@snip [package.json](../snippets/wasmo/package.json) \n\n### WasmRouteMatcher\n\nA route matcher is a plugin that can help the otoroshi router to select a route instance based on your own custom predicate. Basically it's a function that returns a boolean answer.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn matches_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteResponse {\n pub result: bool,\n}\n```\n\n### WasmPreRoute\n\nA pre-route plugin can be used to short-circuit a request or enrich it (maybe extracting your own kind of auth. token, etc) a the very beginning of the request handling process, just after the routing part, when a route has bee chosen by the otoroshi router.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn pre_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteResponse {\n pub error: bool,\n pub attrs: Option>,\n pub status: Option,\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmAccessValidator\n\nAn access validator plugin is typically used to verify if the request can continue or must be cancelled. For instance, the otoroshi apikey plugin is an access validator that check if the current apikey provided by the client is legit and authorized on the current route.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_access(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorContext {\n pub snowflake: Option,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorError {\n pub message: String,\n pub status: u32,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorResponse {\n pub result: bool,\n pub error: Option,\n}\n```\n\n### WasmRequestTransformer\n\nA request transformer plugin can be used to compose or transform the request that will be sent to the backend\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmRequestTransformerContext {\n pub snowflake: Option,\n pub raw_request: OtoroshiRequest,\n pub otoroshi_request: OtoroshiRequest,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n}\n```\n\n### WasmBackendCall\n\nA backend call plugin can be used to simulate a backend behavior in otoroshi. For instance the static backend of otoroshi return the content of a file\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn call_backend(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmBackendContext {\n pub snowflake: Option,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub raw_request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n pub request: OtoroshiRequest,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmBackendResponse {\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n pub status: u32,\n}\n```\n\n### WasmResponseTransformer\n\nA response transformer plugin can be used to compose or transform the response that will be sent back to the client\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_response(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmResponseTransformerContext {\n pub snowflake: Option,\n pub raw_response: OtoroshiResponse,\n pub otoroshi_response: OtoroshiResponse,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub response_body_bytes: Option>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmTransformerResponse {\n pub headers: HashMap,\n pub cookies: Value,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmSink\n\nA sink is a kind of plugin that can be used to respond to any unmatched request before otoroshi sends back a 404 response\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn sink_matches(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn sink_handle(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkContext {\n pub snowflake: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub origin: String,\n pub status: u32,\n pub message: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkMatchesResponse {\n pub result: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkHandleResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmRequestHandler\n\nA request handler is a very special kind of plugin that can bypass the otoroshi proxy engine on specific domains and completely handles the request/response lifecycle on it's own.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerContext {\n pub request: RawRequest\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmJob\n\nA job is a plugin that can run periodically an do whatever you want. Typically, the kubernetes plugins of otoroshi are jobs that periodically sync stuff between otoroshi and kubernetes using the kube-api\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn job_run(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobContext {\n pub attrs: Value,\n pub global_config: Value,\n pub snowflake: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobResult {\n\n}\n```\n\n### Common types\n\n```rs\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Backend {\n pub id: String,\n pub hostname: String,\n pub port: u32,\n pub tls: bool,\n pub weight: u32,\n pub protocol: String,\n pub ip_address: Option,\n pub predicate: Value,\n pub tls_config: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Apikey {\n #[serde(alias = \"clientId\")]\n pub client_id: String,\n #[serde(alias = \"clientName\")]\n pub client_name: String,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct User {\n pub name: String,\n pub email: String,\n pub profile: Value,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RawRequest {\n pub id: u32,\n pub method: String,\n pub headers: HashMap,\n pub cookies: Value,\n pub tls: bool,\n pub uri: String,\n pub path: String,\n pub version: String,\n pub has_body: bool,\n pub remote: String,\n pub client_cert_chain: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Frontend {\n pub domains: Vec,\n pub strict_path: Option,\n pub exact: bool,\n pub headers: HashMap,\n pub query: HashMap,\n pub methods: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct HealthCheck {\n pub enabled: bool,\n pub url: String,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RouteBackend {\n pub targets: Vec,\n pub root: String,\n pub rewrite: bool,\n pub load_balancing: Value,\n pub client: Value,\n pub health_check: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Route {\n pub id: String,\n pub name: String,\n pub description: String,\n pub tags: Vec,\n pub metadata: HashMap,\n pub enabled: bool,\n pub debug_flow: bool,\n pub export_reporting: bool,\n pub capture: bool,\n pub groups: Vec,\n pub frontend: Frontend,\n pub backend: RouteBackend,\n pub backend_ref: Option,\n pub plugins: Value,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct OtoroshiResponse {\n pub status: u32,\n pub headers: HashMap,\n pub cookies: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct OtoroshiRequest {\n pub url: String,\n pub method: String,\n pub headers: HashMap,\n pub version: String,\n pub client_cert_chain: Value,\n pub backend: Option,\n pub cookies: Value,\n}\n```\n\n## Otoroshi interop. with host functions\n\notoroshi provides some host function in order make wasm interact with otoroshi internals. You can\n\n- access wasi resources\n- access http resources\n- access otoroshi internal state\n- access otoroshi internal configuration\n- access otoroshi static configuration\n- access plugin scoped in-memory key/value storage\n- access global in-memory key/value storage\n- access plugin scoped persistent key/value storage\n- access global persistent key/value storage\n\n### authorizations\n\nall the previously listed host functions are enabled with specific authorizations to avoid security issues with third party plugins. You can enable/disable the host function from the wasm plugin entity\n\n@@@ div { .centered-img }\n\n@@@\n\n\n### host functions abi\n\nyou'll find here the raw signatures for the otoroshi host functions. we are currently in the process of writing higher level functions to hide the complexity.\n\nevery time you the the following signature: `(context: u64, size: u64) -> u64` it means that otoroshi is expecting for a pointer to the call context (which is a json string) and it's size. The return is a pointer to the response (which is a json string).\n\nthe signature `(unused: u64) -> u64` means that there is no need for a params but as we technically need one (and hope to don't need one in the future), you have to pass something like `0` as parameter.\n\n```rust\nextern \"C\" {\n // log messages in otoroshi (log levels are 0 to 6 for trace, debug, info, warn, error, critical, max)\n fn proxy_log(logLevel: i32, message: u64, size: u64) -> i32;\n // trigger an otoroshi wasm event that can be exported through data exporters\n fn proxy_log_event(context: u64, size: u64) -> u64;\n // an http client\n fn proxy_http_call(context: u64, size: u64) -> u64;\n // access the current otoroshi state containing a snapshot of all otoroshi entities\n fn proxy_state(context: u64) -> u64;\n fn proxy_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi cluster configuration\n fn proxy_cluster_state(context: u64) -> u64;\n fn proxy_cluster_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi static configuration\n fn proxy_global_config(unused: u64) -> u64;\n // access the current otoroshi dynamic configuration\n fn proxy_config(unused: u64) -> u64;\n // access a persistent key/value store shared by every wasm plugins\n fn proxy_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_datastore_all_matching(context: u64, size: u64) -> u64;\n // access a persistent key/value store for the current plugin instance only\n fn proxy_plugin_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_all_matching(context: u64, size: u64) -> u64;\n // access an in memory key/value store for the current plugin instance only\n fn proxy_plugin_map_set(context: u64, size: u64) -> u64;\n fn proxy_plugin_map_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_map(unused: u64) -> u64;\n // access an in memory key/value store shared by every wasm plugins\n fn proxy_global_map_set(context: u64, size: u64) -> u64;\n fn proxy_global_map_get(context: u64, size: u64) -> u64;\n fn proxy_global_map(unused: u64) -> u64;\n}\n```\n\nright know, when using the Wasmo, a default idiomatic implementation is provided for `TinyGo` and `Rust`\n\nhost.rs\n: @@snip [host.rs](../snippets/wasmo/host.rs) \n\nhost.go\n: @@snip [host.go](../snippets/wasmo/host.go) \n"}] \ No newline at end of file +[{"name":"about.md","id":"/about.md","url":"/about.html","title":"About Otoroshi","content":"# About Otoroshi\n\nAt the beginning of 2017, we had the need to create a new environment to be able to create new \"digital\" products very quickly in an agile fashion at @link:[MAIF](https://www.maif.fr) { open=new }. Naturally we turned to PaaS solutions and chose the excellent @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } product to run our apps. \n\nWe also chose that every feature team will have the freedom to choose its own technological stack to build its product. It was a nice move but it has also introduced some challenges in terms of homogeneity for traceability, security, logging, ... because we did not want to force library usage in the products. We could have used something like @link:[Service Mesh Pattern](http://philcalcado.com/2017/08/03/pattern_service_mesh.html) { open=new } but the deployement model of @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } prevented us to do it.\n\nThe right solution was to use a reverse proxy or some kind of API Gateway able to provide tracability, logging, security with apikeys, quotas, DNS as a service locator, etc. We needed something easy to use, with a human friendly UI, a nice API to extends its features, true hot reconfiguration, able to generate internal events for third party usage. A couple of solutions were available at that time, but not one seems to fit our needs, there was always something missing, too complicated for our needs or not playing well with @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } deployment model.\n\nAt some point, we tried to write a small prototype to explore what could be our dream reverse proxy. The design was very simple, there were some rough edges but every major feature needed was there waiting to be enhanced.\n\n**Otoroshi** was born and we decided to move ahead with our hairy monster :)\n\n## Philosophy \n\nEvery OSS product build at @link:[MAIF](https://www.maif.fr) { open=new } like the developer portal @link:[Daikoku](https://maif.github.io/daikoku) { open=new } or @link:[Izanami](https://maif.github.io/izanami) { open=new } follow a common philosophy. \n\n* the services or API provided should be **technology agnostic**.\n* **http first**: http is the right answer to the previous quote \n* **api First**: the UI is just another client of the api. \n* **secured**: the services exposed need authentication for both humans or machines \n* **event based**: the services should expose a way to get notified of what happened inside. \n"},{"name":"api.md","id":"/api.md","url":"/api.html","title":"Admin REST API","content":"# Admin REST API\n\nOtoroshi provides a fully featured REST admin API to perform almost every operation possible in the Otoroshi dashboard. The Otoroshi dashbaord is just a regular consumer of the admin API.\n\nUsing the admin API, you can do whatever you want and enhance your Otoroshi instances with a lot of features that will feet your needs.\n\n## Swagger descriptor\n\nThe Otoroshi admin API is described using OpenAPI format and is available at :\n\nhttps://maif.github.io/otoroshi/manual/code/openapi.json\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/openapi.json\n\n## Swagger documentation\n\nYou can read the OpenAPI descriptor in a more human friendly fashion using `Swagger UI`. The swagger UI documentation of the Otoroshi admin API is available at :\n\nhttps://maif.github.io/otoroshi/swagger-ui/index.html\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/swagger/ui\n\nYou can also read the swagger UI documentation of the Otoroshi admin API below :\n\n@@@ div { .swagger-frame }\n\n\n@@@\n"},{"name":"architecture.md","id":"/architecture.md","url":"/architecture.html","title":"Architecture","content":"# Architecture\n\nWhen we started the development of Otoroshi, we had several classical patterns in mind like `Service gateway`, `Service locator`, `Circuit breakers`, etc ...\n\nAt start we thought about providing a bunch of librairies that would be included in each microservice or app to perform these tasks. But the more we were thinking about it, the more it was feeling weird, unagile, etc, it also prevented us to use any technical stack we wanted to use. So we decided to change our approach to something more universal.\n\nWe chose to make Otoroshi the central part of our microservices system, something between a reverse-proxy, a service gateway and a service locator where each call to a microservice (even from another microservice) must pass through Otoroshi. There are multiple benefits to do that, each call can be logged, audited, monitored, integrated with a circuit breaker, etc without imposing libraries and technical stack. Any service is exposed through its own domain and we rely only on DNS to handle the service location part. Any access to a service is secured by default with an api key and is supervised by a circuit breaker to avoid cascading failures.\n\n@@@ div { .centered-img }\n\n@@@\n\nOtoroshi tries to embrace our @ref:[global philosophy](./about.md#philosophy) by providing a full featured REST admin api, a gorgeous admin dashboard written in @link:[React](https://reactjs.org) { open=new } that uses the api, by generating traffic events, alerts events, audit events that can be consumed by several channels. Otoroshi also supports a bunch of datastores to better match with different use cases.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"aws.md","id":"/deploy/aws.md","url":"/deploy/aws.html","title":"AWS - Elastic Beanstalk","content":"# AWS - Elastic Beanstalk\n\nNow you want to use Otoroshi on AWS. There are multiple options to deploy Otoroshi on AWS, \nfor instance :\n\n* You can deploy the @ref:[Docker image](../install/get-otoroshi.md#from-docker) on [Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n* You can create a basic [Amazon EC2](https://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/concepts.html), access it via SSH, then \ndeploy the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file) \n* Or you can use [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk)\n\nIn this section we are going to cover how to deploy Otoroshi on [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk). \n\n## AWS Elastic Beanstalk Overview\nUnlike Clever Cloud, to deploy an application on AWS Elastic Beanstalk, you don't link your app to your VCS repository, push your code and expect it to be built and run.\n\nAWS Elastic Beanstalk does only the run part. So you have to handle your own build pipeline, upload a Zip file containing your runnable, then AWS Elastic Beanstalk will take it from there. \n \nEg: for apps running on the JVM (Scala/Java/Kotlin) a Zip with the jar inside would suffice, for apps running in a Docker container, a Zip with the DockerFile would be enough. \n\n\n## Prepare your deployment target\nActually, there are 2 options to build your target. \n\nEither you create a DockerFile from this @ref:[Docker image](../install/get-otoroshi.md#from-docker), build a zip, and do all the Otoroshi custom configuration using ENVs.\n\nOr you download the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file), do all the Otoroshi custom configuration using your own otoroshi.conf, and create a DockerFile that runs the jar using your otoroshi.conf. \n\nFor the second option your DockerFile would look like this :\n\n```dockerfile\nFROM openjdk:11\nVOLUME /tmp\nEXPOSE 8080\nADD otoroshi.jar otoroshi.jar\nADD otoroshi.conf otoroshi.conf\nRUN sh -c 'touch /otoroshi.jar'\nENV JAVA_OPTS=\"\"\nENTRYPOINT [ \"sh\", \"-c\", \"java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dconfig.file=/otoroshi.conf -jar /otoroshi.jar\" ]\n``` \n \nI'd recommend the second option.\n \nNow Zip your target (Jar + Conf + DockerFile) and get ready for deployment. \n\n## Create an Otoroshi instance on AWS Elastic Beanstalk\nFirst, go to [AWS Elastic Beanstalk Console](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/welcome), don't forget to sign in and make sure that you are in the good region (eg : eu-west-3 for Paris).\n\nHit **Get started** \n\n@@@ div { .centered-img }\n\n@@@\n\nSpecify the **Application name** of your application, Otoroshi for example.\n\n@@@ div { .centered-img }\n\n@@@\n \nChoose the **Platform** of the application you want to create, in your case use Docker.\n\nFor **Application code** choose **Upload your code** then hit **Upload**.\n\n@@@ div { .centered-img }\n\n@@@\n\nBrowse the zip created in the [previous section](#prepare-your-deployment-target) from your machine. \n\nAs you can see in the image above, you can also choose an S3 location, you can imagine that at the end of your build pipeline you upload your Zip to S3, and then get it from there (I wouldn't recommend that though).\n \nWhen the upload is done, hit **Configure more options**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nRight now an AWS Elastic Beanstalk application has been created, and by default an environment named Otoroshi-env is being created as well.\n\nAWS Elastic Beanstalk can manage multiple environments of the same application, for instance environments can be (prod, preprod, expriments...). \n\nOtoroshi is a bit particular, it doesn't make much sense to have multiple environments, since Otoroshi will handle all the requests from/to backend services regardless of the environment. \n \nAs you see in the image above, we are now configuring the Otoroshi-env, the one and only environment of Otoroshi.\n \nFor **Configuration presets**, choose custom configuration, now you have a load balancer for your environment with the capacity of at least one instance and at most four.\nI'd recommend at least 2 instances, to change that, on the **Capacity** card hit **Modify**. \n\n@@@ div { .centered-img }\n\n@@@\n\nChange the **Instances** to min 2, max 4 then hit **Save**. For the **Scaling triggers**, I'd keep the default values, but know that you can edit the capacity config any time you want, it only costs a redeploy, which will be done automatically by the way.\n \nInstances size is by default t2.micro, which is a bit small for running Otoroshi, I'd recommend a t2.medium. \nOn the **Instances** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFor **Instance type** choose t2.medium, then hit **Save**, no need to change the volume size, unless you have a lot of http call faults, which means a lot more logs, in that case the default volume size may not be enough.\n\nThe default environment created for Otoroshi, for instance Otoroshi-env, is a web server environment which fits in your case, but the thing is that on AWS Elastic Beanstalk by default a web server environment for a docker-based application, runs behind an Nginx proxy.\nWe have to remove that proxy. So on the **Software** card hit **Modify**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nFor **Proxy server** choose None then hit **Save**.\n\nAlso note that you can set Envs for Otoroshi in same page (see image below). \n\n@@@ div { .centered-img }\n\n@@@ \n\nTo finalise the creation process, hit **Create app** on the bottom right.\n\nThe Otoroshi app is now created, and it's running which is cool, but we still don't have neither a **datastore** nor **https**.\n \n## Create an Otoroshi datastore on AWS ElastiCache\n\nBy default Otoroshi uses non persistent memory to store it's data, Otoroshi supports many kinds of datastores. In this section we will be covering Redis datastore. \n\nBefore starting, using a datastore hosted by AWS is not at all mandatory, feel free to use your own if you like, but if you want to learn more about ElastiCache, this section may interest you, otherwise you can skip it.\n\nGo to [AWS ElastiCache](https://eu-west-3.console.aws.amazon.com/elasticache/home?region=eu-west-3#) and hit **Get Started Now**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nFor **Cluster engine** keep Redis.\n\nChoose a **Name** for your datastore, for instance otoroshi-datastore.\n\nYou can keep all the other default values and hit **Create** on the bottom right of the page.\n\nOnce your Redis Cluster is created, it would look like the image below.\n\n@@@ div { .centered-img }\n\n@@@ \n\n\nFor applications in the same security group as your cluster, redis cluster is accessible via the **Primary Endpoint**. Don't worry the default security group is fine, you don't need any configuration to access the cluster from Otoroshi.\n\nTo make Otoroshi use the created cluster, you can either use Envs `APP_STORAGE=redis`, `REDIS_HOST` and `REDIS_PORT`, or set `otoroshi.storage=redis`, `otoroshi.redis.host` and `otoroshi.redis.port` in your otoroshi.conf.\n\n## Create SSL certificate and configure your domain\n\nOtoroshi has now a datastore, but not yet ready for use. \n\nIn order to get it ready you need to :\n\n* Configure Otoroshi with your domain \n* Create a wildcard SSL certificate for your domain\n* Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n* Configure your DNS to redirect all traffic on your domain to Otoroshi \n \n### Configure Otoroshi with your domain\n\nYou can use ENVs or you can use a custom otoroshi.conf in your Docker container.\n\nFor the second option your otoroshi.conf would look like this :\n\n``` \n include \"application.conf\"\n http.port = 8080\n app {\n env = \"prod\"\n domain = \"mysubdomain.oto.tools\"\n rootScheme = \"https\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n \n storage = \"redis\"\n redis {\n host=\"myredishost\"\n port=myredisport\n }\n \n privateapps {\n subdomain = \"privateapps\"\n }\n \n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-client-id\"\n backOfficeApiKeyClientSecret = \"admin-client-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n proxy {\n https = true\n local = false\n }\n }\n claim {\n sharedKey = \"myclaimsharedkey\"\n }\n }\n \n play.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2147483646\n domain = \".mysubdomain.oto.tools\"\n cookieName = \"oto-sess\"\n }\n }\n``` \n\n### Create a wildcard SSL certificate for your domain\n\nGo to [AWS Certificate Manager](https://eu-west-3.console.aws.amazon.com/acm/home?region=eu-west-3#/firstrun).\n\nBelow **Provision certificates** hit **Get started**.\n\n@@@ div { .centered-img }\n\n@@@ \n \nKeep the default selected value **Request a public certificate** and hit **Request a certificate**.\n \n@@@ div { .centered-img }\n\n@@@ \n\nPut your **Domain name**, use *. for wildcard, for instance *\\*.mysubdomain.oto.tools*, then hit **Next**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nYou can choose between **Email validation** and **DNS validation**, I'd recommend **DNS validation**, then hit **Review**. \n \n@@@ div { .centered-img }\n\n@@@ \n \nVerify that you did put the right **Domain name** then hit **Confirm and request**. \n\n@@@ div { .centered-img }\n\n@@@\n \nAs you see in the image above, to let Amazon do the validation you have to add the `CNAME` record to your DNS configuration. Normally this operation takes around one day.\n \n### Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n\nOnce the certificate is validated, you need to modify the configuration of Otoroshi-env to add the SSL certificate for HTTPS. \nFor that you need to go to [AWS Elastic Beanstalk applications](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/applications),\nhit **Otoroshi-env**, then on the left side hit **Configuration**, then on the **Load balancer** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nIn the **Application Load Balancer** section hit **Add listener**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFill the popup as the image above, then hit **Add**. \n\nYou should now be seeing something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n \n \nMake sure that your listener is enabled, and on the bottom right of the page hit **Apply**.\n\nNow you have **https**, so let's use Otoroshi.\n\n### Configure your DNS to redirect all traffic on your domain to Otoroshi\n \nIt's actually pretty simple, you just need to add a `CNAME` record to your DNS configuration, that redirects *\\*.mysubdomain.oto.tools* to the DNS name of Otoroshi's load balancer.\n\nTo find the DNS name of Otoroshi's load balancer go to [AWS Ec2](https://eu-west-3.console.aws.amazon.com/ec2/v2/home?region=eu-west-3#LoadBalancers:tag:elasticbeanstalk:environment-name=Otoroshi-env;sort=loadBalancerName)\n\nYou would find something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n\nThere is your DNS name, so add your `CNAME` record. \n \nOnce all these steps are done, the AWS Elastic Beanstalk Otoroshi instance, would now be handling all the requests on your domain. ;) \n"},{"name":"clever-cloud.md","id":"/deploy/clever-cloud.md","url":"/deploy/clever-cloud.html","title":"Clever-Cloud","content":"# Clever-Cloud\n\nNow you want to use Otoroshi on Clever Cloud. Otoroshi has been designed and created to run on Clever Cloud and a lot of choices were made because of how Clever Cloud works.\n\n## Create an Otoroshi instance on CleverCloud\n\nIf you want to customize the configuration @ref:[use env. variables](../install/setup-otoroshi.md#configuration-with-env-variables), you can use [the example provided below](#example-of-clevercloud-env-variables)\n\nCreate a new CleverCloud app based on a clevercloud git repo (not empty) or a github project of your own (not empty).\n\n@@@ div { .centered-img }\n\n@@@\n\nThen choose what kind of app your want to create, for Otoroshi, choose `Java + Jar`\n\n@@@ div { .centered-img }\n\n@@@\n\nNext, set up choose instance size and auto-scalling. Otoroshi can run on small instances, especially if you just want to test it.\n\n@@@ div { .centered-img }\n\n@@@\n\nFinally, choose a name for your app\n\n@@@ div { .centered-img }\n\n@@@\n\nNow you just need to customize environnment variables\n\nat this point, you can also add other env. variables to configure Otoroshi like in [the example provided below](#example-of-clevercloud-env-variables)\n\n@@@ div { .centered-img }\n\n@@@\n\nYou can also use expert mode :\n\n@@@ div { .centered-img }\n\n@@@\n\nNow, your app is ready, don't forget to add a custom domains name on the CleverCloud app matching the Otoroshi app domain. \n\n## Example of CleverCloud env. variables\n\nYou can add more env variables to customize your Otoroshi instance like the following. Use the expert mode to copy/paste all the values in one shot. If you want an real datastore, create a redis addon on clevercloud, link it to your otoroshi app and change the `APP_STORAGE` variable to `redis`\n\n
\n\n
\n```\nADMIN_API_CLIENT_ID=xxxx\nADMIN_API_CLIENT_SECRET=xxxxx\nADMIN_API_GROUP=xxxxxx\nADMIN_API_SERVICE_ID=xxxxxxx\nCLAIM_SHAREDKEY=xxxxxxx\nOTOROSHI_INITIAL_ADMIN_LOGIN=youremailaddress\nOTOROSHI_INITIAL_ADMIN_PASSWORD=yourpassword\nPLAY_CRYPTO_SECRET=xxxxxx\nSESSION_NAME=oto-session\nAPP_DOMAIN=yourdomain.tech\nAPP_ENV=prod\nAPP_STORAGE=inmemory\nAPP_ROOT_SCHEME=https\nCC_PRE_BUILD_HOOK=curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/${latest_otoroshi_version}/otoroshi.jar'\nCC_JAR_PATH=./otoroshi.jar\nCC_JAVA_VERSION=11\nPORT=8080\nSESSION_DOMAIN=.yourdomain.tech\nSESSION_MAX_AGE=604800000\nSESSION_SECURE_ONLY=true\nUSER_AGENT=otoroshi\nMAX_EVENTS_SIZE=1\nWEBHOOK_SIZE=100\nAPP_BACKOFFICE_SESSION_EXP=86400000\nAPP_PRIVATEAPPS_SESSION_EXP=86400000\nENABLE_METRICS=true\nOTOROSHI_ANALYTICS_PRESSURE_ENABLED=true\nUSE_CACHE=true\n```\n
"},{"name":"clustering.md","id":"/deploy/clustering.md","url":"/deploy/clustering.html","title":"Otoroshi clustering","content":"# Otoroshi clustering\n\nOtoroshi can work as a cluster by default as you can spin many Otoroshi servers using the same datastore or datastore cluster. In that case any instance is capable of serving services, Otoroshi admin UI, Otoroshi admin API, etc.\n\nBut sometimes, this is not enough. So Otoroshi provides an additional clustering model named `Leader / Workers` where there is a leader cluster ([control plane](https://en.wikipedia.org/wiki/Control_plane)), composed of Otoroshi instances backed by a datastore like Redis, PostgreSQL or Cassandra, that is in charge of all `writes` to the datastore through Otoroshi admin UI and API, and a worker cluster ([data plane](https://en.wikipedia.org/wiki/Forwarding_plane)) composed of horizontally scalable Otoroshi instances, backed by a super fast in memory datastore, with the sole purpose of routing traffic to your services based on data synced from the leader cluster. With this distributed Otoroshi version, you can reach your goals of high availability, scalability and security.\n\nOtoroshi clustering only uses http internally (right now) to make communications between leaders and workers instances so it is fully compatible with PaaS providers like [Clever-Cloud](https://www.clever-cloud.com/en/) that only provide one external port for http traffic.\n\n@@@ div { .centered-img }\n\n\n*Fig. 1: Simplified view*\n@@@\n\n@@@ div { .centered-img }\n\n\n*Fig. 2: Deployment view*\n@@@\n\n## Cluster configuration\n\n```hocon\notoroshi {\n cluster {\n mode = \"leader\" # can be \"off\", \"leader\", \"worker\"\n compression = 4 # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9\n leader {\n name = ${?CLUSTER_LEADER_NAME} # name of the instance, if none, it will be generated\n urls = [\"http://127.0.0.1:8080\"] # urls to contact the leader cluster\n host = \"otoroshi-api.oto.tools\" # host of the otoroshi api in the leader cluster\n clientId = \"apikey-id\" # otoroshi api client id\n clientSecret = \"secret\" # otoroshi api client secret\n cacheStateFor = 4000 # state is cached during (ms)\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME} # name of the instance, if none, it will be generated\n retries = 3 # number of retries when calling leader cluster\n timeout = 2000 # timeout when calling leader cluster\n state {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on state sync\n pollEvery = 10000 # interval of time (ms) between 2 state sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on state sync\n }\n quotas {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on quotas sync\n pushEvery = 2000 # interval of time (ms) between 2 quotas sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on quotas sync\n }\n }\n }\n}\n```\n\nyou can also use many env. variables to configure Otoroshi cluster\n\n```hocon\notoroshi {\n cluster {\n mode = ${?CLUSTER_MODE}\n compression = ${?CLUSTER_COMPRESSION}\n leader {\n name = ${?CLUSTER_LEADER_NAME}\n host = ${?CLUSTER_LEADER_HOST}\n url = ${?CLUSTER_LEADER_URL}\n clientId = ${?CLUSTER_LEADER_CLIENT_ID}\n clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET}\n groupingBy = ${?CLUSTER_LEADER_GROUP_BY}\n cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR}\n stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH}\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME}\n retries = ${?CLUSTER_WORKER_RETRIES}\n timeout = ${?CLUSTER_WORKER_TIMEOUT}\n state {\n retries = ${?CLUSTER_WORKER_STATE_RETRIES}\n pollEvery = ${?CLUSTER_WORKER_POLL_EVERY}\n timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT}\n }\n quotas {\n retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES}\n pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY}\n timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT}\n }\n }\n }\n}\n```\n\n@@@ warning\nYou **should** use HTTPS exposition for the Otoroshi API that will be used for data sync as sensitive informations are exchanged between control plane and data plane.\n@@@\n\n@@@ warning\nYou **must** have the same cluster configuration on every Otoroshi instance (worker/leader) with only names and mode changed for each instance. Some things in leader/worker are computed using configuration of their counterpart worker/leader.\n@@@\n\n## Cluster UI\n\nOnce an Otoroshi instance is launcher as cluster Leader, a new row of live metrics tile will be available on the home page of Otoroshi admin UI.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can also access a more detailed view of the cluster at `Settings (cog icon) / Cluster View`\n\n@@@ div { .centered-img }\n\n@@@\n\n## Run examples\n\nfor leader \n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nfor worker\n\n```sh\njava -Dhttp.port=8092 -Dhttps.port=9092 -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0=http://127.0.0.1:8091 -jar otoroshi.jar\n```\n\n## Setup a cluster by example\n\nif you want to see how to setup an otoroshi cluster, just check @ref:[the clustering tutorial](../how-to-s/setup-otoroshi-cluster.md)"},{"name":"index.md","id":"/deploy/index.md","url":"/deploy/index.html","title":"Deploy to production","content":"# Deploy to production\n\nNow it's time to deploy Otoroshi in production, in this chapter we will see what kind of things you can do.\n\nOtoroshi can run wherever you want, even on a raspberry pi (Cluster^^) ;)\n\n@@@div { .plugin .platform }\n\n## Cloud APIM\n\nCloud APIM provides Otoroshi instances as a service. You can easily create production ready Otoroshi clusters in just a few clics.\n\n\n[Documentation](https://www.cloud-apim.com/)\n@@@\n\n@@@div { .plugin .platform }\n\n## Clever Cloud\n\nOtoroshi provides an integration to create easily services based on application deployed on your Clever Cloud account.\n\n\n@ref:[Documentation](./clever-cloud.md)\n@@@\n\n@@@div { .plugin .platform } \n## Kubernetes\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support.\n\n\n\n@ref:[Documentation](./kubernetes.md)\n@@@\n\n@@@div { .plugin .platform } \n## AWS Elastic Beanstalk\n\nRun Otoroshi on AWS Elastic Beanstalk\n\n\n\n@ref:[Tutorial](./aws.md)\n@@@\n\n@@@div { .plugin .platform } \n## Amazon ECS\n\nDeploy the Otoroshi Docker image using Amazon Elastic Container Service\n\n\n\n@link:[Tutorial](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform }\n## GCE\n\nDeploy the Docker image using Google Compute Engine container integration\n\n\n\n@link:[Documentation](https://cloud.google.com/compute/docs/containers/deploying-containers)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform } \n## Azure\n\nDeploy the Docker image using Azure Container Service\n\n\n\n@link:[Documentation](https://azure.microsoft.com/en-us/services/container-service/)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker) \n@@@\n\n@@@div { .plugin .platform } \n## Heroku\n\nDeploy the Docker image using Docker integration\n\n\n\n@link:[Documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform } \n## CloudFoundry\n\nDeploy the Docker image using -Docker integration\n\n\n\n@link:[Documentation](https://docs.cloudfoundry.org/adminguide/docker.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform .platform-actions-column } \n## Your own infrastructure\n\nAs Otoroshi is a Play Framework application, you can read the doc about putting a `Play` app in production.\n\nDownload the latest Otoroshi distribution, unzip it, customize it and run it.\n\n@link:[Play Framework](https://www.playframework.com)\n@link:[Production Configuration](https://www.playframework.com/documentation/2.6.x/ProductionConfiguration)\n@ref:[Otoroshi distribution](../install/get-otoroshi.md#from-zip)\n@@@\n\n@@@div { .break }\n## Scaling and clustering in production\n@@@\n\n\n@@@div { .plugin .platform .dark-platform } \n## Clustering\n\nDeploy Otoroshi as a cluster of leaders and workers.\n\n\n@ref:[Documentation](./clustering.md)\n@@@\n\n@@@div { .plugin .platform .dark-platform } \n## Scaling Otoroshi\n\nOtoroshi is designed to be reasonably easy to scale and be highly available.\n\n\n@ref:[Documentation](./scaling.md) \n@@@\n\n@@@ index\n\n* [Clustering](./clustering.md)\n* [Kubernetes](./kubernetes.md)\n* [Clever Cloud](./clever-cloud.md)\n* [AWS - Elastic Beanstalk](./aws.md)\n* [Scaling](./scaling.md) \n\n@@@\n"},{"name":"kubernetes.md","id":"/deploy/kubernetes.md","url":"/deploy/kubernetes.html","title":"Kubernetes","content":"# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.15.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.15.0-dev\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"},{"name":"scaling.md","id":"/deploy/scaling.md","url":"/deploy/scaling.html","title":"Scaling Otoroshi","content":"# Scaling Otoroshi\n\n## Using multiple instances with a front load balancer\n\nOtoroshi has been designed to work with multiple instances. If you already have an infrastructure using frontal load balancing, you just have to declare Otoroshi instances as the target of all domain names handled by Otoroshi\n\n## Using master / workers mode of Otoroshi\n\nYou can read everything about it in @ref:[the clustering section](../deploy/clustering.md) of the documentation.\n\n## Using IPVS\n\nYou can use [IPVS](https://en.wikipedia.org/wiki/IP_Virtual_Server) to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi. You can find example of configuration [here](http://www.linuxvirtualserver.org/VS-DRouting.html) \n\n## Using DNS Round Robin\n\nYou can use [DNS round robin technique](https://en.wikipedia.org/wiki/Round-robin_DNS) to declare multiple A records under the domain names handled by Otoroshi.\n\n## Using software L4/L7 load balancers\n\nYou can use software L4 load balancers like NGINX or HAProxy to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi.\n\nNGINX L7\n: @@snip [nginx-http.conf](../snippets/nginx-http.conf) \n\nNGINX L4\n: @@snip [nginx-tcp.conf](../snippets/nginx-tcp.conf) \n\nHA Proxy L7\n: @@snip [haproxy-http.conf](../snippets/haproxy-http.conf) \n\nHA Proxy L4\n: @@snip [haproxy-tcp.conf](../snippets/haproxy-tcp.conf) \n\n## Using a custom TCP load balancer\n\nYou can also use any other TCP load balancer, from a hardware box to a small js file like\n\ntcp-proxy.js\n: @@snip [tcp-proxy.js](../snippets/tcp-proxy.js) \n\ntcp-proxy.rs\n: @@snip [tcp-proxy.rs](../snippets/proxy.rs) \n\n"},{"name":"dev.md","id":"/dev.md","url":"/dev.html","title":"Developing Otoroshi","content":"# Developing Otoroshi\n\nIf you want to play with Otoroshis code, here are some tips\n\n## The tools\n\nYou will need\n\n* git\n* JDK >= 11\n* SBT >= 1.7+\n* Node 18+ & yarn 1.x\n\n## Clone the repository\n\n```sh\ngit clone https://github.com/MAIF/otoroshi.git\n```\n\nor fork otoroshi and clone your own repository.\n\n## Run otoroshi in dev mode\n\nto run otoroshi in dev mode, you'll need to run two separate process to serve the javascript UI and the server part.\n\n### Javascript side\n\njust go to `/otoroshi/javascript` and install the dependencies with\n\n```sh\nyarn install\n# or\nnpm install\n```\n\nthen run the dev server with\n\n```sh\nyarn start\n# or\nnpm run start\n```\n\n### Server side\n\nsetup SBT opts with\n\n```sh\nexport SBT_OPTS=\"-Xmx2G -Xss6M\"\n```\n\nthen just go to `/otoroshi` and run the sbt console with \n\n```sh\nsbt\n```\n\nthen in the sbt console run the following command\n\n```sh\n~reStart\n# to pass jvm args, you can use: ~reStart --- -Dotoroshi.storage=memory ...\n```\n\nyou can now access your otoroshi instance at `http://otoroshi.oto.tools:9999`\n\n## Test otoroshi\n\nto run otoroshi test just go to `/otoroshi` and run the main test suite with\n\n```sh\nsbt 'testOnly OtoroshiTests'\n```\n\n## Create a release\n\njust go to `/otoroshi/javascript` and then build the UI\n\n```sh\nyarn install\nyarn build\n```\n\nthen go to `/otoroshi` and build the otoroshi distribution\n\n```sh\nsbt ';clean;compile;dist;assembly'\n```\n\nthe otoroshi build is waiting for you in `/otoroshi/target/scala-2.12/otoroshi.jar` or `/otoroshi/target/universal/otoroshi-1.x.x.zip`\n\n## Build the documentation\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/doc.sh all\n```\n\nThe documentation is located at `manual/target/paradox/site/main/`\n\n## Format the sources\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/fmt.sh\n```\n"},{"name":"apikeys.md","id":"/entities/apikeys.md","url":"/entities/apikeys.html","title":"Apikeys","content":"# Apikeys\n\nAn API key is a unique identifier used to connect to, or perform, an route call. \n\n@@@ div { .centered-img }\n\n@@@\n\nYou can found a concrete example @ref:[here](../how-to-s/secure-with-apikey.md)\n\n* `ApiKey Id`: the id is a unique random key that will represent this API key\n* `ApiKey Secret`: the secret is a random key used to validate the API key\n* `ApiKey Name`: a name for the API key, used for debug purposes\n* `ApiKey description`: a useful description for this apikey\n* `Valid until`: auto disable apikey after this date\n* `Enabled`: if the API key is disabled, then any call using this API key will fail\n* `Read only`: if the API key is in read only mode, every request done with this api key will only work for GET, HEAD, OPTIONS verbs\n* `Allow pass by clientid only`: here you allow client to only pass client id in a specific header in order to grant access to the underlying api\n* `Constrained services only`: this apikey can only be used on services using apikey routing constraints\n* `Authorized on`: the groups/services linked to this api key\n\n### Metadata and tags\n\n* `Tags`: tags attached to the api key\n* `Metadata`: metadata attached to the api key\n\n### Automatic secret rotation\n\nAPI can handle automatic secret rotation by themselves. When enabled, the rotation changes the secret every `Rotation every` duration. During the `Grace period` both secret will be usable.\n \n* `Enabled`: enabled automatic apikey secret rotation\n* `Rotation every`: rotate secrets every\n* `Grace period`: period when both secrets can be used\n* `Next client secret`: display the next generated client secret\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Call examples\n\n* `Curl Command`: simple request with the api key passed by header\n* `Basic Auth. Header`: authorization Header with the api key as base64 encoded format\n* `Curl Command with Basic Auth. Header`: simple request with api key passed in the Authorization header as base64 format\n\n### Quotas\n\n* `Throttling quota`: the authorized number of calls per second\n* `Daily quota`: the authorized number of calls per day\n* `Monthly quota`: the authorized number of calls per month\n\n@@@ warning\n\nDaily and monthly quotas are based on the following rules :\n\n* daily quota is computed between 00h00:00.000 and 23h59:59.999 of the current day\n* monthly qutoas is computed between the first day of the month at 00h00:00.000 and the last day of the month at 23h59:59.999\n@@@\n\n### Quotas consumption\n\n* `Consumed daily calls`: the number of calls consumed today\n* `Remaining daily calls`: the remaining number of calls for today\n* `Consumed monthly calls`: the number of calls consumed this month\n* `Remaining monthly calls`: the remaining number of calls for this month\n\n"},{"name":"auth-modules.md","id":"/entities/auth-modules.md","url":"/entities/auth-modules.html","title":"Authentication modules","content":"# Authentication modules\n\nThe authentication modules manage the access to Otoroshi UI and can protect a route.\n\nA `private Otoroshi app` is an Otoroshi route with the Authentication plugin enabled.\n\nThe list of supported authentication are :\n\n* `OAuth 2.0/2.1` : an authorization standard that allows a user to grant limited access to their resources on one site to another site, without having to expose their credentials\n* `OAuth 1.0a` : the original standard for access delegation\n* `In memory` : create users directly in Otoroshi with rights and metadata\n* `LDAP : Lightweight Directory Access Protocol` : connect users using a set of LDAP servers\n* `SAML V2 - Security Assertion Markup Language` : an open-standard, XML-based data format that allows businesses to communicate user authentication and authorization information to partner companies and enterprise applications their employees may use.\n\nAll authentication modules have a unique `id`, a `name` and a `description`.\n\nEach module has also the following fields : \n\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n* `HttpOnly`: if enabled, the cookie cannot be accessed through client side script, prevent cross-site scripting (XSS) by not revealing the cookie to a third party\n* `Secure`: if enabled, avoid to include cookie in an HTTP Request without secure channel, typically HTTPs.\n* `Session max. age`: duration until the session expired\n* `User validators`: a list of validator that will check if, a user that successfully logged in has the right to actually, pass otoroshi based on the content of it's profile. A validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value. The JSONPath will be applied on a document that will look like\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"randomId\": \"xxxxx\",\n \"name\": \"john.doe@otoroshi.io\",\n \"email\": \"john.doe@otoroshi.io\",\n \"authConfigId\": \"xxxxxxxx\",\n \"profile\": { // the profile shape depends heavily on the identity provider\n \"sub\": \"xxxxxx\",\n \"nickname\": \"john.doe\",\n \"name\": \"john.doe@otoroshi.io\",\n \"picture\": \"https://foo.bar/avatar.png\",\n \"updated_at\": \"2022-04-20T12:57:39.723Z\",\n \"email\": \"john.doe@otoroshi.io\",\n \"email_verified\": true,\n \"rights\": [\"one\", \"two\"]\n },\n \"token\": { // the token shape depends heavily on the identity provider\n \"access_token\": \"xxxxxx\",\n \"refresh_token\": \"yyyyyy\",\n \"id_token\": \"zzzzzz\",\n \"scope\": \"openid profile email address phone offline_access\",\n \"expires_in\": 86400,\n \"token_type\": \"Bearer\"\n },\n \"realm\": \"global-oauth-xxxxxxx\",\n \"otoroshiData\": {\n ...\n },\n \"createdAt\": 1650459462650,\n \"expiredAt\": 1650545862652,\n \"lastRefresh\": 1650459462650,\n \"metadata\": {},\n \"tags\": []\n}\n```\n\nthe expected value support some syntax tricks like \n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current user has the right `two`, you can write the following validator\n\n```js\n{\n \"path\": \"$.profile.rights\",\n \"value\": \"Contains(two)\"\n}\n```\n\n## OAuth 2.0 / OIDC provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check these tutorials : @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md) or @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n\n* `Use cookie`: If your OAuth2 provider does not support query param in redirect uri, you can use cookies instead\n* `Use json payloads`: the access token, sended to retrieve the user info, will be pass in body as JSON. If disabled, it will sended as Map.\n* `Enabled PKCE flow`: This way, a malicious attacker can only intercept the Authorization Code, and they cannot exchange it for a token without the Code Verifier.\n* `Disable wildcard on redirect URIs`: As of OAuth 2.1, query parameters on redirect URIs are no longer allowed\n* `Refresh tokens`: Automatically refresh access token using the refresh token if available\n* `Read profile from token`: if enabled, the user profile will be read from the access token, otherwise the user profile will be retrieved from the user information url\n* `Super admins only`: All logged in users will have super admins rights\n* `Client ID`: a public identifier of your app\n* `Client Secret`: a secret known only to the application and the authorization server\n* `Authorize URL`: used to interact with the resource owner and get the authorization to access the protected resource\n* `Token URL`: used by the application in order to get an access token or a refresh token\n* `Introspection URL`: used to validate access tokens\n* `Userinfo URL`: used to retrieve the profile of the user\n* `Login URL`: used to redirect user to the login provider page\n* `Logout URL`: redirect uri used by the identity provider to redirect user after logging out\n* `Callback URL`: redirect uri sended to the identity provider to redirect user after successfully connecting\n* `Access token field name`: field used to search access token in the response body of the token URL call\n* `Scope`: presented scopes to the user in the consent screen. Scopes are space-separated lists of identifiers used to specify what access privileges are being requested\n* `Claims`: asked name/values pairs that contains information about a user.\n* `Name field name`: Retrieve name from token field\n* `Email field name`: Retrieve email from token field\n* `Otoroshi metadata field name`: Retrieve metadata from token field\n* `Otoroshi rights field name`: Retrieve user rights from user profile\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n* `Api key metadata field name`: used to extract api key metadata from the OIDC access token \n* `Api key tags field name`: used to extract api key tags from the OIDC access token \n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n* `OIDC config url`: URI of the openid-configuration used to discovery documents. By convention, this URI ends with `.well-known/openid-configuration`\n* `Token verification`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: The Hmac secret\n* `Base64 encoded secret`: Is the secret encoded with base64\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n## OAuth 1.0a provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)\n\n* `Http Method`: method used to get request token and the access token \n* `Consumer key`: the identifier portion of the client credentials (equivalent to a username)\n* `Consumer secret`: the identifier portion of the client credentials (equivalent to a password)\n* `Request Token URL`: url to retrieve the request token\n* `Authorize URL`: used to redirect user to the login page\n* `Access token URL`: used to retrieve the access token from the server\n* `Profile URL`: used to get the user profile\n* `Callback URL`: used to redirect user when successfully connecting\n* `Rights override`: override the rights of the connected user. With JSON format, each authenticated user, using email, can be associated to a list of rights on tenants and Otoroshi teams.\n\n## LDAP Authentication provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Allow empty password`: LDAP servers configured by default with the possibility to connect without password can be secured by this module to ensure that user provides a password\n* `Super admins only`: All logged in users will have super admins rights\n* `Extract profile`: extract LDAP profile in the Otoroshi user\n* `LDAP Server URL`: list of LDAP servers to join. Otoroshi use this list in sequence and swap to the next server, each time a server breaks in timeout\n* `Search Base`: used to global filter\n* `Users search base`: concat with search base to search users in LDAP\n* `Mapping group filter`: map LDAP groups with Otoroshi rights\n* `Search Filter`: used to filter users. *\\${username}* is replace by the email of the user and compare to the given field\n* `Admin username (bind DN)`: holds the name of the environment property for specifying the identity of the principal for authenticating the caller to the service\n* `Admin password`: holds the name of the environment property for specifying the credentials of the principal for authenticating the caller to the service\n* `Extract profile filters attributes in`: keep only attributes which are matching the regex\n* `Extract profile filters attributes not in`: keep only attributes which are not matching the regex\n* `Name field name`: Retrieve name from LDAP field\n* `Email field name`: Retrieve email from LDAP field\n* `Otoroshi metadata field name`: Retrieve metadata from LDAP field\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Additional rights group`: list of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n\n## In memory provider\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Login with WebAuthn` : enabled logging by WebAuthn\n* `Users`: list of users with *name*, *email* and *metadata*. The default password is *password*. The edit button is useful when you want to change the password of the user. The reset button reinitialize the password. \n* `Users raw`: show the registered users with their profile and their rights. You can edit directly each field, especially the rights of the user.\n\n## SAML v2 provider\n\n* `Single sign on URL`: the Identity Provider Single Sign-On URL\n* `The protocol binding for the login request`: the protocol binding for the login request\n* `Single Logout URL`: a SAML flow that allows the end-user to logout from a single session and be automatically logged out of all related sessions that were established during SSO\n* `The protocol binding for the logout request`: the protocol binding for the logout request\n* `Sign documents`: Should SAML Request be signed by Otoroshi ?\n* `Validate Assertions Signature`: Enable/disable signature validation of SAML assertions\n* `Validate assertions with Otoroshi certificate`: validate assertions with Otoroshi certificate. If disabled, the `Encryption Certificate` and `Encryption Private Key` fields can be used to pass a certificate and a private key to validate assertions.\n* `Encryption Certificate`: certificate used to verify assertions\n* `Encryption Private Key`: privaye key used to verify assertions\n* `Signing Certificate`: certicate used to sign documents\n* `Signing Private Key`: private key to sign documents\n* `Signature al`: the signature algorithm to use to sign documents\n* `Canonicalization Method`: canonicalization method for XML signatures \n* `Encryption KeyPair`: the keypair used to sign/verify assertions\n* `Name ID Format`: SP and IdP usually communicate each other about a subject. That subject should be identified through a NAME-IDentifier, which should be in some format so that It is easy for the other party to identify it based on the Format\n* `Use NameID format as email`: use NameID format as email. If disabled, the email will be search from the attributes\n* `URL issuer`: provide the URL to the IdP's who will issue the security token\n* `Validate Signature`: enable/disable signature validation of SAML responses\n* `Validate Assertions Signature`: should SAML Assertions to be decrypted ?\n* `Validating Certificates`: the certificate in PEM format that must be used to check for signatures.\n\n## Special routes\n\nwhen using private apps with auth. modules, you can access special routes that can help you \n\n```sh \nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/logout' # trigger logout for the current auth. module\nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/me' # get the current logged user profile (do not forget to pass cookies)\n```\n\n## Related pages\n* @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n* @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md)\n* @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n* @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)"},{"name":"backends.md","id":"/entities/backends.md","url":"/entities/backends.html","title":"Backends","content":"# Backends\n\nA backend represent a list of server to target in a route and its client settings, load balancing, etc.\n\nThe backends can be define directly on the route designer or on their dedicated page in order to be reusable.\n\n## UI page\n\nYou can find all backends [here](http://otoroshi.oto.tools:8080/bo/dashboard/backends)\n\n## Global Properties\n\n* `Targets root path`: the path to add to each request sent to the downstream service \n* `Full path rewrite`: When enabled, the path of the uri will be totally stripped and replaced by the value of `Targets root path`. If this value contains expression language expressions, they will be interpolated before forwading the request to the backend. When combined with things like named path parameters, it is possible to perform a ful url rewrite on the target path like\n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Targets\n\nThe list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures.\n\n* `id`: unique id of the target\n* `Hostname`: the hostname of the target without scheme\n* `Port`: the port of the target\n* `TLS`: call the target via https\n* `Weight`: the weight of the target. This valus is used by the load balancing strategy to dispatch the traffic between all targets\n* `Predicate`: a function to filter targets from the target list based on a predefined predicate\n* `Protocol`: protocol used to call the target, can be only equals to `HTTP/1.0`, `HTTP/1.1`, `HTTP/2.0` or `HTTP/3.0`\n* `IP address`: the ip address of the target\n* `TLS Settings`:\n * `Enabled`: enable this section\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with the downstream service\n * `Trusted certificates`: list of trusted certificates received from the downstream service\n\n\n## Heatlh check\n\n* `Enabled`: if enabled, the health check URL will be called at regular intervals\n* `URL`: the URL to call to run the health check\n\n## Load balancing\n\n* `Type`: the load balancing algorithm used\n\n## Client settings\n\n* `backoff factor`: specify the factor to multiply the delay for each retry (default value 2)\n* `retries`: specify how many times the client will retry to fetch the result of the request after an error before giving up. (default value 1)\n* `max errors`: specify how many errors can pass before opening the circuit breaker (default value 20)\n* `global timeout`: specify how long the global call (with retries) should last at most in milliseconds. (default value 30000)\n* `connection timeout`: specify how long each connection should last at most in milliseconds. (default value 10000)\n* `idle timeout`: specify how long each connection can stay in idle state at most in milliseconds (default value 60000)\n* `call timeout`: Specify how long each call should last at most in milliseconds. (default value 30000)\n* `call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response. (default value 120000)\n* `initial delay`: delay after which first retry will happens if needed (default value 50)\n* `sample interval`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor (default value 2000)\n* `cache connection`: try to keep tcp connection alive between requests (default value false)\n* `cache connection queue size`: queue size for an open tcp connection (default value 2048)\n* `custom timeouts` (list): \n * `Path`: the path on which the timeout will be active\n * `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n * `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n * `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n * `Call timeout`: Specify how long each call should last at most in milliseconds.\n * `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n## Proxy\n\n* `host`: host of proxy behind the identify provider\n* `port`: port of proxy behind the identify provider\n* `protocol`: protocol of proxy behind the identify provider\n* `principal`: user of proxy \n* `password`: password of proxy\n"},{"name":"certificates.md","id":"/entities/certificates.md","url":"/entities/certificates.html","title":"Certificates","content":"# Certificates\n\nAll generated and imported certificates are listed in the `https://otoroshi.xxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe list of available actions are:\n\n* `Add item`: redirects the user on the certificate creation page. It's useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* `Let's Encrypt certificate`: asks a certificate matching a given host to Let's encrypt \n* `Create certificate`: issues a certificate with an existing Otoroshi certificate as CA.\n* `Import .p12 file`: loads a p12 file as certificate\n\n## Add item\n\n* `Id`: the generated unique id of the certificate\n* `Name`: the name of the certificate\n* `Description`: the description of the certificate\n* `Auto renew cert.`: certificate will be issued when it will be expired. Only works with a CA from Otoroshi and a known private key\n* `Client cert.`: the certificate generated will be used to identicate a client to a server\n* `Keypair`: the certificate entity will be a pair of public key and private key.\n* `Public key exposed`: if true, the public key will be exposed on `http://otoroshi-api.your-domain/.well-known/jwks.json`\n* `Certificate status`: the current status of the certificate. It can be valid if the certificate is not revoked and not expired, or equal to the reason of the revocation\n* `Certificate full chain`: list of certificates used to authenticate a client or a server\n* `Certificate private key`: the private key of the certificate or nothing if wanted. You can omit it if you want just add a certificte full chain to trust them.\n* `Private key password`: the password to protect the private key\n* `Certificate tags`: the tags attached to the certificate\n* `Certaificate metadata`: the metadata attached to the certificate\n\n## Let's Encrypt certificate\n\n* `Let's encrypt`: if enabled, the certificate will be generated by Let's Encrypt. If disabled, the user will be redirect to the `Create certificate` page\n* `Host`: the host send to Let's encrypt to issue the certificate\n\n## Create certificate view\n\n* `Issuer`: the CA used to sign your certificate\n* `CA certificate`: if enabled, the certificate will be used as an authority certificate. Once generated, it will be use as CA to sign the new certificates\n* `Let's Encrypt`: redirects to the Let's Encrypt page to request a certificate\n* `Client certificate`: the certificate generated will be used to identicate a client to a server\n* `Include A.I.A`: include authority information access urls in the certificate\n* `Key Type`: the type of the private key\n* `Key Size`: the size of the private key\n* `Signature Algorithm`: the signature algorithm used to sign the certificate\n* `Digest Algorithm`: the digest algorithm used\n* `Validity`: how much time your certificate will be valid\n* `Subject DN`: the subject DN of your certificate\n* `Hosts`: the hosts of your certificate\n\n"},{"name":"data-exporters.md","id":"/entities/data-exporters.md","url":"/entities/data-exporters.html","title":"Data exporters","content":"# Data exporters\n\nThe data exporters are the way to export alerts and events from Otoroshi to an external storage.\n\nTo try them, you can folllow @ref[this tutorial](../how-to-s/export-alerts-using-mailgun.md).\n\n## Common fields\n\n* `Type`: the type of event exporter\n* `Enabled`: enabled or not the exporter\n* `Name`: given name to the exporter\n* `Description`: the data exporter description\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nAll exporters are split in three parts. The first and second parts are common and the last are specific by exporter.\n\n* `Filtering and projection` : section to filter the list of sent events and alerts. The projection field allows you to export only certain event fields and reduce the size of exported data. It's composed of `Filtering` and `Projection` fields. To get a full usage of this elements, read @ref:[this section](#matching-and-projections)\n* `Queue details`: set of fields to adjust the workers of the exporter. \n * `Buffer size`: if elements are pushed onto the queue faster than the source is consumed the overflow will be handled with a strategy specified by the user. Keep in memory the number of events.\n * `JSON conversion workers`: number of workers used to transform events to JSON format in paralell\n * `Send workers`: number of workers used to send transformed events\n * `Group size`: chunk up this stream into groups of elements received within a time window (the time window is the next field)\n * `Group duration`: waiting time before sending the group of events. If the group size is reached before the group duration, the events will be instantly sent\n \nFor the last part, the `Exporter configuration` will be detail individually.\n\n## Matching and projections\n\n**Filtering** is used to **include** or **exclude** some kind of events and alerts. For each include and exclude field, you can add a list of key-value. \n\nLet's say we only want to keep Otoroshi alerts\n```json\n{ \"include\": [{ \"@type\": \"AlertEvent\" }] }\n```\n\nOtoroshi provides a list of rules to keep only events with specific values. We will use the following event to illustrate.\n\n```json\n{\n \"foo\": \"bar\",\n \"type\": \"AlertEvent\",\n \"alert\": \"big-alert\",\n \"status\": 200,\n \"codes\": [\"a\", \"b\"],\n \"inner\": {\n \"foo\": \"bar\",\n \"bar\": \"foo\"\n }\n}\n```\n\nThe rules apply with the previous example as event.\n\n@@@div { #filtering }\n \n@@@\n\n\n\n**Projection** is a list of fields to export. In the case of an empty list, all the fields of an event will be exported. In other case, **only** the listed fields will be exported.\n\nLet's say we only want to keep Otoroshi alerts and only type, timestamp and id of each exported events\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nAn other possibility is to **rename** the exported field. This value will be the same but the exported field will have a different name.\n\nLet's say we want to rename all `@id` field with `unique-id` as key\n\n```json\n{ \"@id\": \"unique-id\" }\n```\n\nThe last possiblity is to retrieve a sub-object of an event. Let's say we want to get the name of each exported user of events.\n\n```json\n{ \"user\": { \"name\": true } }\n```\n\nYou can also expand the entire source object with \n\n```json\n{\n \"$spread\": true\n}\n```\n\nand the remove fields you don't want with \n\n```json\n{\n \"fieldthatidontwant\": false\n}\n```\n\nProjections allows object modification using jspath, for instance, this example will create a new `otoroshiHeaderKeys` field to exported events. This field will contains a string array containing every request header name.\n\n```json\n{\n \"otoroshiHeaderKeys\": {\n \"$path\": \"$.otoroshiHeadersIn.*.key\"\n }\n}\n```\n\nAlternativerly, projections also allow to use JQ to transform exported events\n\n```json\n{\n \"headerKeys\": {\n \"$jq\": \"[.headers[].key]\"\n }\n}\n```\n\nJQ filter also allows conditionnal filtering : transformation is applied only if given predicate is match. In the following example, `headerKeys` field will be valued only if `target.scheme` is `https`.\n\n```json\n{\n \"headerKeys\": {\n \"$jqIf\": {\n \"filter\": \"[.headers[].key]\",\n \"predicate\": {\n \"path\": \"target.scheme\",\n \"value\": \"https\"\n }\n }\n }\n}\n```\n\nSee [JQ manual](https://jqlang.github.io/jq/manual/) for complete syntax reference.\n\n## Elastic\n\nWith this kind of exporter, every matching event will be sent to an elastic cluster (in batch). It is quite useful and can be used in combination with [elastic read in global config](./global-config.html#analytics-elastic-dashboard-datasource-read-)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with checked point, and if the case of it's successfull, it will displayed the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template presents in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n## Webhook \n\nWith this kind of exporter, every matching event will be sent to a URL (in batch) using a POST method and an JSON array body.\n\n* `Alerts hook URL`: url used to post events\n* `Hook Headers`: headers add to the post request\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n## Pulsar \n\nWith this kind of exporter, every matching event will be sent to an [Apache Pulsar topic](https://pulsar.apache.org/)\n\n\n* `Pulsar URI`: URI of the pulsar server\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n* `Pulsar tenant`: tenant on the pulsar server\n* `Pulsar namespace`: namespace on the pulsar server\n* `Pulsar topic`: topic on the pulsar server\n\n## Kafka \n\nWith this kind of exporter, every matching event will be sent to an [Apache Kafka topic](https://kafka.apache.org/). You can find few @ref[tutorials](../how-to-s/communicate-with-kafka.md) about the connection between Otoroshi and Kafka based on docker images.\n\n* `Kafka Servers`: the list of servers to contact to connect the Kafka client with the Kafka cluster\n* `Kafka topic`: the topic on which Otoroshi alerts will be sent\n\nBy default, Kafka is installed with no authentication. Otoroshi supports the following authentication mechanisms and protocols for Kafka brokers.\n\n### SASL\n\nThe Simple Authentication and Security Layer (SASL) [RFC4422] is a\nmethod for adding authentication support to connection-based\nprotocols.\n\n* `SASL username`: the client username \n* `SASL password`: the client username \n* `SASL Mechanism`: \n * `PLAIN`: SASL/PLAIN uses a simple username and password for authentication.\n * `SCRAM-SHA-256` and `SCRAM-SHA-512`: SASL/SCRAM uses usernames and passwords stored in ZooKeeper. Credentials are created during installation.\n\n### SSL \n\n* `Kafka keypass`: the keystore password if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka keystore path`: the keystore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka truststore path`: the truststore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Custom TLS Settings`: enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n### SASL + SSL\n\nThis mechanism uses the SSL configuration and the SASL configuration.\n\n## Mailer \n\nWith this kind of exporter, every matching event will be sent in batch as an email (using one of the following email provider)\n\nOtoroshi supports 5 exporters of email type.\n\n### Console\n\nNothing to add. The events will be write on the standard output.\n\n### Generic\n\n* `Mailer url`: URL used to push events\n* `Headers`: headers add to the push requests\n* `Email addresses`: recipients of the emails\n\n### Mailgun\n\n* `EU`: is EU server ? if enabled, *https://api.eu.mailgun.net/* will be used, otherwise, the US URL will be used : *https://api.mailgun.net/*\n* `Mailgun api key`: API key of the mailgun account\n* `Mailgun domain`: domain name of the mailgun account\n* `Email addresses`: recipients of the emails\n\n### Mailjet\n\n* `Public api key`: public key of the mailjet account\n* `Private api key`: private key of the mailjet account\n* `Email addresses`: recipients of the emails\n\n### Sendgrid\n\n* `Sendgrid api key`: api key of the sendgrid account\n* `Email addresses`: recipients of the emails\n\n## File \n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n\n## GoReplay file\n\nWith this kind of exporter, every matching event will be sent to a `.gor` file compatible with [GoReplay](https://goreplay.org/). \n\n@@@ warning\nthis exporter will only be able to catch `TrafficCaptureEvent`. Those events are created when a route (or the global config) of the @ref:[new proxy engine](../topics/engine.md) is setup to capture traffic using the `capture` flag.\n@@@\n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n* `Capture requests`: capture http requests in the `.gor` file\n* `Capture responses`: capture http responses in the `.gor` file\n\n## Console \n\nNothing to add. The events will be write on the standard output.\n\n## Custom \n\nThis type of exporter let you the possibility to write your own exporter with your own rules. To create an exporter, we need to navigate to the plugins page, and to create a new item of type exporter.\n\nWhen it's done, the exporter will be visible in this list.\n\n* `Exporter config.`: the configuration of the custom exporter.\n\n## Metrics \n\nThis plugin is useful to rewrite the metric labels exposed on the `/metrics` endpoint.\n\n* `Labels`: list of metric labels. Each pair contains an existing field name and the new name."},{"name":"global-config.md","id":"/entities/global-config.md","url":"/entities/global-config.html","title":"Global config","content":"# Global config\n\nThe global config, named `Danger zone` in Otoroshi, is the place to configure Otoroshi globally. \n\n> Warning: In this page, the configuration is really sensitive and affects the global behaviour of Otoroshi.\n\n\n### Misc. Settings\n\n\n* `Maintenance mode` : It passes every single service in maintenance mode. If a user calls a service, the maintenance page will be displayed\n* `No OAuth login for BackOffice` : Forces admins to login only with user/password or user/password/u2F device\n* `API Read Only`: Freeze Otoroshi datastore in read only mode. Only people with access to the actual underlying datastore will be able to disable this.\n* `Auto link default` : When no group is specified on a service, it will be assigned to default one\n* `Use circuit breakers` : Use circuit breaker on all services\n* `Use new http client as the default Http client` : All http calls will use the new http client by default\n* `Enable live metrics` : Enable live metrics in the Otoroshi cluster. Performs a lot of writes in the datastore\n* `Digitus medius` : Use middle finger emoji as a response character for endless HTTP responses (see [IP address filtering settings](#ip-address-filtering-settings)).\n* `Limit conc. req.` : Limit the number of concurrent request processed by Otoroshi to a certain amount. Highly recommended for resilience\n* `Use X-Forwarded-* headers for routing` : When evaluating routing of a request, X-Forwarded-* headers will be used if presents\n* `Max conc. req.` : Maximum number of concurrent requests processed by otoroshi.\n* `Max HTTP/1.0 resp. size` : Maximum size of an HTTP/1.0 response in bytes. After this limit, response will be cut and sent as is. The best value here should satisfy (maxConcurrentRequests * maxHttp10ResponseSize) < process.memory for worst case scenario.\n* `Max local events` : Maximum number of events stored.\n* `Lines` : *deprecated* \n\n### IP address filtering settings\n\n* `IP allowed list`: Only IP addresses that will be able to access Otoroshi exposed services\n* `IP blocklist`: IP addresses that will be refused to access Otoroshi exposed services\n* `Endless HTTP Responses`: IP addresses for which each request will return around 128 Gb of 0s\n\n\n### Quotas settings\n\n* `Global throttling`: The max. number of requests allowed per second globally on Otoroshi\n* `Throttling per IP`: The max. number of requests allowed per second per IP address globally on Otoroshi\n\n### Analytics: Elastic dashboard datasource (read)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with a connection checklist, if connection is successfull, it will display the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template present in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes over time. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `TrustAll`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with elasticsearch\n* `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n### Statsd settings\n\n* `Datadog agent`: The StatsD agent is a Datadog agent\n* `StatsD agent host`: The host on which StatsD agent is listening\n* `StatsD agent port`: The port on which StatsD agent is listening (default is 8125)\n\n\n### Backoffice auth. settings\n\n* `Backoffice auth. config`: the authentication module used in front of Otoroshi. It will be used to connect to Otoroshi on the login page\n\n### Let's encrypt settings\n\n* `Enabled`: when enabled, Otoroshi will have the possiblity to sign certificate from let's encrypt notably in the SSL/TSL Certificates page \n* `Server URL`: ACME endpoint of let's encrypt \n* `Email addresses`: (optional) list of addresses used to order the certificates \n* `Contact URLs`: (optional) list of addresses used to order the certificates \n* `Public Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n* `Private Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n\n\n### CleverCloud settings\n\nOnce configured, you can register one clever cloud app of your organization directly as an Otoroshi service.\n\n* `CleverCloud consumer key`: consumer key of your clever cloud OAuth 1.0 app\n* `CleverCloud consumer secret`: consumer secret of your clever cloud OAuth 1.0 app\n* `OAuth Token`: oauth token of your clever cloud OAuth 1.0 app\n* `OAuth Secret`: oauth token secret of your clever cloud OAuth 1.0 app \n* `CleverCloud orga. Id`: id of your clever cloud organization\n\n### Global scripts\n\nGlobal scripts will be deprecated soon, please use global plugins instead (see the next section)!\n\n### Global plugins\n\n* `Enabled`: enable the use of global plugins\n* `Plugins on new Otoroshi engine`: list of plugins used by the new Otoroshi engine\n* `Plugins on old Otoroshi engine`: list of plugins used by the old Otoroshi engine\n* `Plugin configuration`: the overloaded configuration of plugins\n\n### Proxies\n\nIn this section, you can add a list of proxies for :\n\n* Proxy for alert emails (mailgun)\n* Proxy for alert webhooks\n* Proxy for Clever-Cloud API access\n* Proxy for services access\n* Proxy for auth. access (OAuth, OIDC)\n* Proxy for client validators\n* Proxy for JWKS access\n* Proxy for elastic access\n\nEach proxy has the following fields \n\n* `Proxy host`: host of proxy\n* `Proxy port`: port of proxy\n* `Proxy principal`: user of proxy\n* `Proxy password`: password of proxy\n* `Non proxy host`: IP address that can access the service\n\n### Quotas alerting settings\n\n* `Enable quotas exceeding alerts`: When apikey quotas is almost exceeded, an alert will be sent \n* `Daily quotas threshold`: The percentage of daily calls before sending alerts\n* `Monthly quotas threshold`: The percentage of monthly calls before sending alerts\n\n### User-Agent extraction settings\n\n* `User-Agent extraction`: Allow user-agent details extraction. Can have impact on consumed memory. \n\n### Geolocation extraction settings\n\nExtract a geolocation for each call to Otoroshi.\n\n### Tls Settings\n\n* `Use random cert.`: Use the first available cert when none matches the current domain\n* `Default domain`: When the SNI domain cannot be found, this one will be used to find the matching certificate \n* `Trust JDK CAs (server)`: Trust JDK CAs. The CAs from the JDK CA bundle will be proposed in the certificate request when performing TLS handshake \n* `Trust JDK CAs (trust)`: Trust JDK CAs. The CAs from the JDK CA bundle will be used as trusted CAs when calling HTTPS resources \n* `Trusted CAs (server)`: Select the trusted CAs you want for TLS terminaison. Those CAs only will be proposed in the certificate request when performing TLS handshake \n\n\n### Auto Generate Certificates\n\n* `Enabled`: Generate certificates on the fly when they don't exist\n* `Reply Nicely`: When receiving request from a not allowed domain name, accept connection and display a nice error message \n* `CA`: certificate CA used to generate missing certificate\n* `Allowed domains`: Allowed domains\n* `Not allowed domains`: Not allowed domains\n \n\n### Global metadata\n\n* `Tags`: tags attached to the global config\n* `Metadata`: metadata attached to the global config\n\n### Actions at the bottom of the page\n\n* `Recover from a full export file`: Load global configuration from a previous export\n* `Full export`: Export with all created entities\n* `Full export (ndjson)`: Export your full state of database to ndjson format\n* `JSON`: Get the global config at JSON format \n* `YAML`: Get the global config at YAML format \n* `Enable Panic Mode`: Log out all users from UI and prevent any changes to the database by setting the admin Otoroshi api to read-only. The only way to exit of this mode is to disable this mode directly in the database. "},{"name":"index.md","id":"/entities/index.md","url":"/entities/index.html","title":"","content":"\n# Main entities\n\nIn this section, we will pass through all the main Otoroshi entities. Otoroshi entities are the main items stored in otoroshi datastore that will be used to configure routing, authentication, etc.\n\nAny entity has the following properties\n\n* **location** or **\\_loc**: the location of the entity (organization and team)\n* **id**: the id of the entity (except for apikeys)\n* **name**: the name of the entity\n* **description**: the description of the entity (optional)\n* **tags**: free tags that you can put on any entity to help you manage it, automate it, etc.\n* **metadata**: free key/value tuples that you can put on any entity to help you manage it, automate it, etc.\n\n@@@div { .entities }\n\n
\nRoutes\nProxy your applications with routes\n
\n@ref:[View](./routes.md)\n@@@\n\n@@@div { .entities }\n\n
\nBackends\nReuse route targets\n
\n@ref:[View](./backends.md)\n@@@\n\n@@@div { .entities }\n\n
\nApikeys\nAdd security to your services using apikeys\n
\n@ref:[View](./apikeys.md)\n@@@\n\n\n@@@div { .entities }\n\n
\nOrganizations\nThis the most high level for grouping resources.\n
\n@ref:[View](./organizations.md)\n@@@\n\n@@@div { .entities }\n\n
\nTeams\nOrganize your resources by teams\n
\n@ref:[View](./teams.md)\n@@@\n\n@@@div { .entities }\n\n
\nService groups\nGroup your services\n
\n@ref:[View](./service-groups.md)\n@@@\n\n@@@div { .entities }\n\n
\nJWT verifiers\nVerify and forge token by services.\n
\n@ref:[View](./jwt-verifiers.md)\n@@@\n\n@@@div { .entities }\n\n
\nGlobal Config\nThe danger zone of Otoroshi\n
\n@ref:[View](./global-config.md)\n@@@\n\n@@@div { .entities }\n\n
\nTCP services\n\n
\n@ref:[View](./tcp-services.md)\n@@@\n\n@@@div { .entities }\n\n
\nAuth. modules\nSecure the Otoroshi UI and your web apps\n
\n@ref:[View](./auth-modules.md)\n@@@\n\n@@@div { .entities }\n\n
\nCertificates\nAdd secure communication between Otoroshi, clients and services\n
\n@ref:[View](./certificates.md)\n@@@\n\n@@@div { .entities }\n\n
\nData exporters\nExport alerts, events ands logs\n
\n@ref:[View](./data-exporters.md)\n@@@\n\n@@@div { .entities }\n\n
\nScripts\n\n
\n@ref:[View](./scripts.md)\n@@@\n\n@@@div { .entities }\n\n
\nService descriptors\nProxy your applications with service descriptors\n
\n@ref:[View](./service-descriptors.md)\n@@@\n\n@@@ index\n\n* [Routes](./routes.md)\n* [Backends](./backends.md)\n* [Organizations](./organizations.md)\n* [Teams](./teams.md)\n* [Global Config](./global-config.md)\n* [Apikeys](./apikeys.md)\n* [Service groups](./service-groups.md)\n* [Auth. modules](./auth-modules.md)\n* [Certificates](./certificates.md)\n* [JWT verifiers](./jwt-verifiers.md)\n* [Data exporters](./data-exporters.md)\n* [Scripts](./scripts.md)\n* [TCP services](./tcp-services.md)\n* [Service descriptors](./service-descriptors.md)\n\n@@@\n"},{"name":"jwt-verifiers.md","id":"/entities/jwt-verifiers.md","url":"/entities/jwt-verifiers.html","title":"JWT verifiers","content":"# JWT verifiers\n\nSometimes, it can be pretty useful to verify Jwt tokens coming from other provider on some services. Otoroshi provides a tool to do that per service.\n\n* `Name`: name of the JWT verifier\n* `Description`: a simple description\n* `Strict`: if not strict, request without JWT token will be allowed to pass. This option is helpful when you want to force the presence of tokens in each request on a specific service \n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nEach JWT verifier is configurable in three steps : the `location` where find the token in incoming requests, the `validation` step to check the signature and the presence of claims in tokens, and the last step, named `Strategy`.\n\n## Token location\n\nAn incoming token can be found in three places.\n\n#### In query string\n\n* `Source`: JWT token location in query string\n* `Query param name`: the name of the query param where JWT is located\n\n#### In a header\n\n* `Source`: JWT token location in a header\n* `Header name`: the name of the header where JWT is located\n* `Remove value`: when the token is read, this value will be remove of header value (example: if the header value is *Bearer xxxx*, the *remove value* could be Bearer  don't forget the space at the end of the string)\n\n#### In a cookie\n\n* `Source`: JWT token location in a cookie\n* `Cookie name`: the name of the cookie where JWT is located\n\n## Token validation\n\nThis section is used to verify the extracted token from specified location.\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n\nAccording to the selected algorithm, the validation form will change.\n\n#### Hmac + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n#### RSASSA-PKCS1 + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the RSA public key\n* `Private key`: the RSA private key that can be empty if not used for JWT token signing\n\n#### ECDSA + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the ECDSA public key\n* `Private key`: the ECDSA private key that can be empty if not used for JWT token signing\n\n#### RSASSA-PKCS1 + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n \n#### ECDSA + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n\n#### Otoroshi KeyPair from token kid (only for verification)\n* `Use only exposed keypairs`: if enabled, Otoroshi will only use the key pairs that are exposed on the well-known. If disabled, it will search on any registered key pairs.\n\n#### JWK Set (only for verification)\n\n* `URL`: the JWK set URL where the public keys are exposed\n* `HTTP call timeout`: timeout for fetching the keyset\n* `TTL`: cache TTL for the keyset\n* `HTTP Headers`: the HTTP headers passed\n* `Key type`: type of the key searched in the jwks\n\n*TLS settings for JWKS fetching*\n\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n*Proxy*\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n## Strategy\n\nThe first step is to select the verifier strategy. Otoroshi supports 4 types of JWT verifiers:\n\n* `Default JWT token` will add a token if no present. \n* `Verify JWT token` will only verifiy token signing and fields values if provided. \n* `Verify and re-sign JWT token` will verify the token and will re-sign the JWT token with the provided algo. settings. \n* `Verify, re-sign and transform JWT token` will verify the token, re-sign and will be able to transform the token.\n\nAll verifiers has the following properties: \n\n* `Verify token fields`: when the JWT token is checked, each field specified here will be verified with the provided value\n* `Verify token array value`: when the JWT token is checked, each field specified here will be verified if the provided value is contained in the array\n\n\n#### Default JWT token\n\n* `Strict`: if token is already present, the call will fail\n* `Default value`: list of claims of the generated token. These fields support raw values or language expressions. See the documentation about @ref:[the expression language](../topics/expression-language.md)\n\n#### Verify JWT token\n\nNo specific values needed. This kind of verifier needs only the two fields `Verify token fields` and `Verify token array value`.\n\n#### Verify and re-sign JWT token\n\nWhen `Verify and re-sign JWT token` is chosen, the `Re-sign settings` appear. All fields of `Re-sign settings` are the same of the `Token validation` section. The only difference is that the values are used to sign the new token and not to validate the token.\n\n\n#### Verify, re-sign and transform JWT token\n\nWhen `Verify, re-sign and transform JWT token` is chosen, the `Re-sign settings` and `Transformation settings` appear.\n\nThe `Re-sign settings` are used to sign the new token and has the same fields than the `Token validation` section.\n\nFor the `Transformation settings` section, the fields are:\n\n* `Token location`: the location where to find/set the JWT token\n* `Header name`: the name of the header where JWT is located\n* `Prepend value`: remove a value inside the header value\n* `Rename token fields`: when the JWT token is transformed, it is possible to change a field name, just specify origin field name and target field name\n* `Set token fields`: when the JWT token is transformed, it is possible to add new field with static values, just specify field name and value\n* `Remove token fields`: when the JWT token is transformed, it is possible to remove fields"},{"name":"organizations.md","id":"/entities/organizations.md","url":"/entities/organizations.html","title":"Organizations","content":"# Organizations\n\nThe resources of Otoroshi are grouped by `Organization`. This the highest level for grouping resources.\n\nAn organization have a unique `id`, a `name` and a `description`. As all Otoroshi resources, an Organization have a list of tags and metadata associated.\n\nFor example, you can use the organizations as a mean of :\n\n* to seperate resources by services or entities in your enterprise\n* to split internal and external usage of the resources (it's useful when you have a list of services deployed in your company and another one deployed by your partners)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of organizations\n\nTo visualize and edit the list of organizations, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/organizations` route or click on the cog icon and select the organizations button.\n\nOnce on the page, you can create a new item, edit an existing organization or delete an existing one.\n\n> When an organization is deleted, the resources associated are not deleted. On the other hand, the organization and the team of associated resources are let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of one organization (`tenant` in the json document)\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": ...\n }\n ...\n}\n```\n\nor all organizations\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"*\",\n \"teams\": ...\n }\n ...\n}\n```\n\n"},{"name":"routes.md","id":"/entities/routes.md","url":"/entities/routes.html","title":"Routes","content":"# Routes\n\nA route is an unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins and eventually forward the request to the backend application.\n\n## UI page\n\nYou can find all routes [here](http://otoroshi.oto.tools:8080/bo/dashboard/routes)\n\n## Global Properties\n\n* `location`: the location of the entity\n* `id`: the id of the route\n* `name`: the name of the route\n* `description`: the description of the route\n* `tags`: the tags of the route. can be useful for api automation\n* `metadata`: the metadata of the route. can be useful for api automation. There are a few reserved metadata used by otorshi that can be found @ref[below](./routes.md#reserved-metadata)\n* `enabled`: is the route enabled ? if not, the router will not consider this route\n* `debugFlow`: the debug flag. If enabled, the execution report for this route will contain all input/output values through steps of the proxy engine. For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `capture`: if enabled, otoroshi will generate events containing the whole content of each request. Use with caution ! For more informations, check the @ref[engine documentation](../topics/engine.md#http-traffic-capture)\n* `exportReporting`: if enabled, execution reports of the proxy engine will be generated for each request. Those reports are exportable using @ref[data exporters](./data-exporters.md) . For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `groups`: each route is attached to a group. A group can have one or more services/routes. Each API key is linked to groups/routes/services and allow access to every entities in the groups.\n\n### Reserved metadata\n\nsome metadata are reserved for otoroshi usage. Here is the list of reserved metadata\n\n* `otoroshi-core-user-facing`: is this a user facing app for the snow monkey\n* `otoroshi-core-use-akka-http-client`: use the pure akka http client\n* `otoroshi-core-use-netty-http-client`: use the pure netty http client\n* `otoroshi-core-use-akka-http-ws-client`: use the modern websocket client\n* `otoroshi-core-issue-lets-encrypt-certificate`: enabled let's encrypt certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate`: enabled certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate-ca`: the id of the CA cert to generate the certificate for this route\n* `otoroshi-core-openapi-url`: the openapi url for this route\n* `otoroshi-core-env`: the env for this route. here for legacy reasons\n* `otoroshi-deployment-providers`: in the case of relay routing, the providers for this route\n* `otoroshi-deployment-regions`: in the case of relay routing, the network regions for this route\n* `otoroshi-deployment-zones`: in the case of relay routing, the network zone for this route \n* `otoroshi-deployment-dcs`: in the case of relay routing, the datacenter for this route \n* `otoroshi-deployment-racks`: in the case of relay routing, the rack for this route \n\n## Frontend configuration\n\n* `frontend`: the frontend of the route. It's the configuration that will configure how otoroshi router will match this route. A frontend has the following shape. \n\n```javascript\n{\n \"domains\": [ // the matched domains and paths\n \"new-route.oto.tools/path\" // here you can use wildcard in domain and path, also you can use named path params\n ],\n \"strip_path\": true, // is the matched path stripped in the forwarded request\n \"exact\": false, // perform exact matching on path, if not, will be matched on /path*\n \"headers\": {}, // the matched http headers. if none provided, any header will be matched\n \"query\": {}, // the matched http query params. if none provided, any query params will be matched\n \"methods\": [] // the matched http methods. if none provided, any method will be matched\n}\n```\n\nFor more informations about routing, check the @ref[engine documentation](../topics/engine.md#routing)\n\n## Backend configuration\n\n* `backend`: a backend to forward requests to. For more informations, go to the @ref[backend documentation](./backends.md)\n* `backendRef`: a reference to an existing backend id\n\n## Plugins\n\nthe liste of plugins used on this route. Each plugin definition has the following shape:\n\n```javascript\n{\n \"enabled\": false, // is the plugin enabled\n \"debug\": false, // is debug enabled of this specific plugin\n \"plugin\": \"cp:otoroshi.next.plugins.Redirection\", // the id of the plugin\n \"include\": [], // included paths. if none, all paths are included\n \"exclude\": [], // excluded paths. if none, none paths are excluded\n \"config\": { // the configuration of the plugin\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"plugin_index\": { // the position of the plugin. if none provided, otoroshi will use the order in the plugin array\n \"pre_route\": 0\n }\n}\n```\n\nfor more informations about the available plugins, go @ref[here](../plugins/built-in-plugins.md)\n\n\n"},{"name":"scripts.md","id":"/entities/scripts.md","url":"/entities/scripts.html","title":"Scripts","content":"# Scripts\n\nScript are a way to create plugins for otoroshi without deploying them as jar files. With scripts, you just have to store the scala code of your plugins inside the otoroshi datastore and otoroshi will compile and deploy them at startup. You can find all your scripts in the UI at `cog icon / Plugins`. You can find all the documentation about plugins @ref:[here](../plugins/index.md)\n\n@@@ warning\nThe compilation of your plugins can be pretty long and resources consuming. As the compilation happens during otoroshi boot sequence, your instance will be blocked until all plugins have compiled. This behavior can be disabled. If so, the plugins will not work until they have been compiled. Any service using a plugin that is not compiled yet will fail\n@@@\n\nLike any entity, the script has has the following properties\n\n* `id`\n* `plugin name`\n* `plugin description`\n* `tags`\n* `metadata`\n\nAnd you also have\n\n* `type`: the kind of plugin you are building with this script\n* `plugin code`: the code for your plugin\n\n## Compile\n\nYou can use the compile button to check if the code you write in `plugin code` is valid. It will automatically save your script and try to compile. As mentionned earlier, script compilation is quite resource intensive. It will affect your CPU load and your memory consumption. Don't forget to adjust your VM settings accordingly.\n"},{"name":"service-descriptors.md","id":"/entities/service-descriptors.md","url":"/entities/service-descriptors.html","title":"Service descriptors","content":"# Service descriptors\n\nServices or service descriptor, let you declare how to proxy a call from a domain name to another domain name (or multiple domain names). \n\n@@@ div { .centered-img }\n\n@@@\n\nLet’s say you have an API exposed on http://192.168.0.42 and I want to expose it on https://my.api.foo. Otoroshi will proxy all calls to https://my.api.foo and forward them to http://192.168.0.42. While doing that, it will also log everyhting, control accesses, etc.\n\n\n* `Id`: a unique random string to identify your service\n* `Groups`: each service descriptor is attached to a group. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group.\n* `Create a new group`: you can create a new group to host this descriptor\n* `Create dedicated group`: you can create a new group with an auto generated name to host this descriptor\n* `Name`: the name of your service. Only for debug and human readability purposes.\n* `Description`: the description of your service. Only for debug and human readability purposes.\n* `Service enabled`: activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist.\n* `Read only mode`: authorize only GET, HEAD, OPTIONS calls on this service\n* `Maintenance mode`: display a maintainance page when a user try to use the service\n* `Construction mode`: display a construction page when a user try to use the service\n* `Log analytics`: Log analytics events for this service on the servers\n* `Use new http client`: will use Akka Http Client for every request\n* `Detect apikey asap`: If the service is public and you provide an apikey, otoroshi will detect it and validate it. Of course this setting may impact performances because of useless apikey lookups.\n* `Send Otoroshi headers back`: when enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...\n* `Override Host header`: when enabled, Otoroshi will automatically set the Host header to corresponding target host\n* `Send X-Forwarded-* headers`: when enabled, Otoroshi will send X-Forwarded-* headers to target\n* `Force HTTPS`: will force redirection to `https://` if not present\n* `Allow HTTP/1.0 requests`: will return an error on HTTP/1.0 request\n* `Use new WebSocket client`: will use the new websocket client for every websocket request\n* `TCP/UDP tunneling`: with this setting enabled, otoroshi will not proxy http requests anymore but instead will create a secured tunnel between a cli on your machine and otoroshi to proxy any tcp connection with all otoroshi security features enabled\n\n### Service exposition settings\n\n* `Exposed domain`: the domain used to expose your service. Should follow pattern: `(http|https)://subdomain?.env?.domain.tld?/root?` or regex `(http|https):\\/\\/(.*?)\\.?(.*?)\\.?(.*?)\\.?(.*)\\/?(.*)`\n* `Legacy domain`: use `domain`, `subdomain`, `env` and `matchingRoot` for routing in addition to hosts, or just use hosts.\n* `Strip path`: when matching, strip the matching prefix from the upstream request URL. Defaults to true\n* `Issue Let's Encrypt cert.`: automatically issue and renew let's encrypt certificate based on domain name. Only if Let's Encrypt enabled in global config.\n* `Issue certificate`: automatically issue and renew a certificate based on domain name\n* `Possible hostnames`: all the possible hostnames for your service\n* `Possible matching paths`: all the possible matching paths for your service\n\n### Redirection\n\n* `Redirection enabled`: enabled the redirection. If enabled, a call to that service will redirect to the chosen URL\n* `Http redirection code`: type of redirection used\n* `Redirect to`: URL used to redirect user when the service is called\n\n### Service targets\n\n* `Redirect to local`: if you work locally with Otoroshi, you may want to use that feature to redirect one specific service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests\n* `Load balancing`: the load balancing algorithm used\n* `Targets`: the list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n* `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is `/api/foo`, then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n\n### URL Patterns\n\n* `Make service a 'public ui'`: add a default pattern as public routes\n* `Make service a 'private api'`: add a default pattern as private routes\n* `Public patterns`: by default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use `/.*`\n* `Private patterns`: if you define a public pattern that is a little bit too much, you can make some of public URL private again\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Otoroshi exchange protocol\n\n* `Enabled`: when enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside.\n* `Send challenge`: when disbaled, Otoroshi will not check if target service respond with sent random value.\n* `Send info. token`: when enabled, Otoroshi add an additional header containing current call informations\n* `Challenge token version`: version the otoroshi exchange protocol challenge. This option will be set to V2 in a near future.\n* `Info. token version`: version the otoroshi exchange protocol info token. This option will be set to Latest in a near future.\n* `Tokens TTL`: the number of seconds for tokens (state and info) lifes\n* `State token header name`: the name of the header containing the state token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.state)\n* `State token response header name`: the name of the header containing the state response token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.stateresp)\n* `Info token header name`: the name of the header containing the info token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.claim)\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Use same algo.`: when enabled, all JWT token in this section will use the same signing algorithm. If `use same algo.` is disabled, three more options will be displayed to select an algorithm for each step of the calls :\n * Otoroshi to backend\n * Backend to otoroshi\n * Info. token\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n### Authentication\n\n* `Enforce user authentication`: when enabled, user will be allowed to use the service (UI) only if they are registered users of the chosen authentication module.\n* `Auth. config`: authentication module used to protect the service\n* `Create a new auth config.`: navigate to the creation of authentication module page\n* `all auth config.`: navigate to the authentication pages\n\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Strict mode`: strict mode enabled\n\n### Api keys constraints\n\n* `From basic auth.`: you can pass the api key in Authorization header (ie. from 'Authorization: Basic xxx' header)\n* `Allow client id only usage`: you can pass the api key using client id only (ie. from Otoroshi-Token header)\n* `From custom headers`: you can pass the api key using custom headers (ie. Otoroshi-Client-Id and Otoroshi-Client-Secret headers)\n* `From JWT token`: you can pass the api key using a JWT token (ie. from 'Authorization: Bearer xxx' header)\n\n#### Basic auth. Api Key\n\n* `Custom header name`: the name of the header to get Authorization\n* `Custom query param name`: the name of the query param to get Authorization\n\n#### Client ID only Api Key\n\n* `Custom header name`: the name of the header to get the client id\n* `Custom query param name`: the name of the query param to get the client id\n\n#### Custom headers Api Key\n\n* `Custom client id header name`: the name of the header to get the client id\n* `Custom client secret header name`: the name of the header to get the client secret\n\n#### JWT Token Api Key\n\n* `Secret signed`: JWT can be signed by apikey secret using HMAC algo.\n* `Keypair signed`: JWT can be signed by an otoroshi managed keypair using RSA/EC algo.\n* `Include Http request attrs.`: if enabled, you have to put the following fields in the JWT token corresponding to the current http call (httpPath, httpVerb, httpHost)\n* `Max accepted token lifetime`: the maximum number of second accepted as token lifespan\n* `Custom header name`: the name of the header to get the jwt token\n* `Custom query param name`: the name of the query param to get the jwt token\n* `Custom cookie name`: the name of the cookie to get the jwt token\n\n### Routing constraints\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata\n\n### CORS support\n\n* `Enabled`: if enabled, CORS header will be check for each incoming request\n* `Allow credentials`: if enabled, the credentials will be sent. Credentials are cookies, authorization headers, or TLS client certificates.\n* `Allow origin`: if enabled, it will indicates whether the response can be shared with requesting code from the given\n* `Max age`: response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.\n* `Expose headers`: response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.\n* `Allow headers`: response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.\n* `Allow methods`: response header specifies one or more methods allowed when accessing a resource in response to a preflight request.\n* `Excluded patterns`: by default, when cors is enabled, everything has cors. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n#### Related documentations\n\n* @link[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) { open=new }\n* @link[Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) { open=new }\n* @link[Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) { open=new }\n* @link[Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) { open=new }\n* @link[Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) { open=new }\n\n### JWT tokens verification\n\n* `Verifiers`: list of selected verifiers to apply on the service\n* `Enabled`: if enabled, Otoroshi will enabled each verifier of the previous list\n* `Excluded patterns`: list of routes where the verifiers will not be apply\n\n### Pre Routing\n\nThis part has been deprecated and moved to the plugin section.\n\n### Access validation\nThis part has been deprecated and moved to the plugin section.\n\n### Gzip support\n\n* `Mimetypes allowed list`: gzip only the files that are matching to a format in the list\n* `Mimetypes blocklist`: will not gzip files matching to a format in the list. A possible way is to allowed all format by default by setting a `*` on the `Mimetypes allowed list` and to add the unwanted format in this list.\n* `Compression level`: the compression level where 9 gives us maximum compression but at the slowest speed. The default compression level is 5 and is a good compromise between speed and compression ratio.\n* `Buffer size`: chunking up a stream of bytes into limited size\n* `Chunk threshold`: if the content type of a request reached over the threshold, the response will be chunked\n* `Excluded patterns`: by default, when gzip is enabled, everything has gzip. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n### Client settings\n\n* `Use circuit breaker`: use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !\n* `Cache connections`: use a cache at host connection level to avoid reconnection time\n* `Client attempts`: specify how many times the client will retry to fetch the result of the request after an error before giving up.\n* `Client call timeout`: specify how long each call should last at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n* `C.breaker max errors`: specify how many errors can pass before opening the circuit breaker\n* `C.breaker retry delay`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor\n* `C.breaker backoff factor`: specify the factor to multiply the delay for each retry\n* `C.breaker window`: specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted\n\n#### Custom timeout settings (list)\n\n* `Path`: the path on which the timeout will be active\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Call timeout`: Specify how long each call should last at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n#### Proxy settings\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n### HTTP Headers\n\n* `Additional Headers In`: specify headers that will be added to each client request (from Otoroshi to target). Useful to add authentication.\n* `Additional Headers Out`: specify headers that will be added to each client response (from Otoroshi to client).\n* `Missing only Headers In`: specify headers that will be added to each client request (from Otoroshi to target) if not in the original request.\n* `Missing only Headers Out`: specify headers that will be added to each client response (from Otoroshi to client) if not in the original response.\n* `Remove incoming headers`: remove headers in the client request (from client to Otoroshi).\n* `Remove outgoing headers`: remove headers in the client response (from Otoroshi to client).\n* `Security headers`:\n* `Utility headers`:\n* `Matching Headers`: specify headers that MUST be present on client request to route it (pre routing). Useful to implement versioning.\n* `Headers verification`: verify that some headers has a specific value (post routing)\n\n### Additional settings \n\n* `OpenAPI`: specify an open API descriptor. Useful to display the documentation\n* `Tags`: specify tags for the service\n* `Metadata`: specify metadata for the service. Useful for analytics\n* `IP allowed list`: IP address that can access the service\n* `IP blocklist`: IP address that cannot access the service\n\n### Canary mode\n\n* `Enabled`: Canary mode enabled\n* `Traffic split`: Ratio of traffic that will be sent to canary targets. For instance, if traffic is at 0.2, for 10 request, 2 request will go on canary targets and 8 will go on regular targets.\n* `Targets`: The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n * `Target`:\n * `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n* `Campaign stats`:\n* `Use canary targets as standard targets`:\n\n### Healthcheck settings\n\n* `HealthCheck enabled`: to help failing fast, you can activate healthcheck on a specific URL.\n* `HealthCheck url`: the URL to check. Should return an HTTP 200 response. You can also respond with an 'Opun-Health-Check-Logic-Test-Result' header set to the value of the 'Opun-Health-Check-Logic-Test' request header + 42. to make the healthcheck complete.\n\n### Fault injection\n\n* `User facing app.`: if service is set as user facing, Snow Monkey can be configured to not being allowed to create outage on them.\n* `Chaos enabled`: activate or deactivate chaos setting on this service descriptor.\n\n### Custom errors template\n\n* `40x template`: html template displayed when 40x error occurred\n* `50x template`: html template displayed when 50x error occurred\n* `Build mode template`: html template displayed when the build mode is enabled\n* `Maintenance mode template`: html template displayed when the maintenance mode is enabled\n* `Custom messages`: override error message one by one\n\n### Request transformation\n\nThis part has been deprecated and moved to the plugin section.\n\n### Plugins\n\n* `Plugins`:\n \n * `Inject default config`: injects, if present, the default configuration of a selected plugin in the configuration object\n * `Documentation`: link to the documentation website of the plugin\n * `show/hide config. panel`: shows and hides the plugin panel which contains the plugin description and configuration\n* `Excluded patterns`: by default, when plugins are enabled, everything pass in. But sometimes you need to exclude something, so just add regex to matching path you want to exlude.\n* `Configuration`: the configuration of each enabled plugin, split by names and grouped in the same configuration object."},{"name":"service-groups.md","id":"/entities/service-groups.md","url":"/entities/service-groups.html","title":"Service groups","content":"# Service groups\n\nA service group is composed of an unique `id`, a `Group name`, a `Group description`, an `Organization` and a `Team`. As all Otoroshi resources, a service group have a list of tags and metadata associated.\n\n@@@ div { .centered-img }\n\n@@@\n\nThe first instinctive usage of service group is to group a list of services. \n\nWhen it's done, you can authorize an api key on a specific group. Instead of authorize an api key for each service, you can regroup a list of services together, and give authorization on the group (read the page on the api keys and the usage of the `Authorized on.` field).\n\n## Access to the list of service groups\n\nTo visualize and edit the list of groups, you can navigate to your instance on the `https://otoroshi.xxxxx/bo/dashboard/groups` route or click on the cog icon and select the Service groups button.\n\nOnce on the page, you can create a new item, edit an existing service group or delete an existing one.\n\n> When a service group is deleted, the resources associated are not deleted. On the other hand, the service group of associated resources is let empty.\n\n"},{"name":"tcp-services.md","id":"/entities/tcp-services.md","url":"/entities/tcp-services.html","title":"TCP services","content":"# TCP services\n\nTCP service are special kind of otoroshi services meant to proxy pure TCP connections (ssh, database, http, etc)\n\n## Global information\n\n* `Id`: generated unique identifier\n* `TCP service name`: the name of your TCP service\n* `Enabled`: enable and disable the service\n* `TCP service port`: the listening port\n* `TCP service interface`: network interface listen by the service\n* `Tags`: list of tags associated to the service\n* `Metadata`: list of metadata associated to the service\n\n## TLS\n\nthis section controls the TLS exposition of the service\n\n* `TLS mode`\n * `Disabled`: no TLS\n * `PassThrough`: as the target exposes TLS, the call will pass through otoroshi and use target TLS\n * `Enabled`: the service will be exposed using TLS and will chose certificate based on SNI\n* `Client Auth.`\n * `None` no mTLS needed to pass\n * `Want` pass with or without mTLS\n * `Need` need mTLS to pass\n\n## Server Name Indication (SNI)\n\nthis section control how SNI should be treated\n\n* `SNI routing enabled`: if enabled, the server will use the SNI hostname to determine which certificate to present to the client\n* `Forward to target if no SNI match`: if enabled, a call without any SNI match will be forward to the target\n* `Target host`: host of the target called if no SNI\n* `Target ip address`: ip of the target called if no SNI\n* `Target port`: port of the target called if no SNI\n* `TLS call`: encrypt the communication with TLS\n\n## Rules\n\nfor any listening TCP proxy, it is possible to route to multiple targets based on SNI or extracted http host (if proxying http)\n\n* `Matching domain name`: regex used to filter the list of domains where the rule will be applied\n* `Target host`: host of the target\n* `Target ip address`: ip of the target\n* `Target port`: port of the target\n* `TLS call`: enable this flag if the target is exposed using TLS\n"},{"name":"teams.md","id":"/entities/teams.md","url":"/entities/teams.html","title":"Teams","content":"# Teams\n\nIn Otoroshi, all resources are attached to an `Organization` and a `Team`. \n\nA team is composed of an unique `id`, a `name`, a `description` and an `Organization`. As all Otoroshi resources, a Team have a list of tags and metadata associated.\n\nA team have an unique organization and can be use on multiples resources (services, api keys, etc ...).\n\nA connected user on Otoroshi UI has a list of teams and organizations associated. It can be helpful when you want restrict the rights of a connected user.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of teams\n\nTo visualize and edit the list of teams, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/teams` route or click on the cog icon and select the teams button.\n\nOnce on the page, you can create a new item, edit an existing team or delete an existing one.\n\n> When a team is deleted, the resources associated are not deleted. On the other hand, the team of associated resources is let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of multiple teams in an organization\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"team-1\",\n \"team-2\"\n ]\n }\n ...\n}\n```\n\nor all teams\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"*\"\n ]\n }\n ...\n}\n```"},{"name":"features.md","id":"/features.md","url":"/features.html","title":"Features","content":"# Features\n\n**Traffic Management**\n\n* Can proxy any HTTP(s) service (apis, webapps, websocket, etc)\n* Can proxy any TCP service (app, database, etc)\n* Can proxy any GRPC service\n* Multiple load-balancing options: \n * RoundRobin\n * Random, Sticky\n * Ip address hash\n * Best Response Time\n* Distributed in-flight request limiting\t\n* Distributed rate limiting \n* End-to-end HTTP/1.1 support\n* End-to-end H2 support\n* End-to-end H3 support\n* Traffic mirroring\n* Traffic capture\n* Canary deployments\n* Relay routing \n* Tunnels for easier network exposition\n* Error templates\n\n**Routing**\n\n* Router can support ten of thousands of concurrent routes\n* Router support path params extraction (can be regex validated)\n* Routing based on \n * method\n * hostname (exact, wildcard)\n * path (exact, wildcard)\n * header values (exact, regex, wildcard)\n * query param values (exact, regex, wildcard)\n* Support full url rewriting\n\n**Routes customization**\n\n* Dozens of built-in middlewares (policies/plugins) \n * circuit breakers\n * automatic retries\n * buffering\n * gzip\n * headers manipulation\n * cors\n * body transformation\n * graphql gateway\n * etc \n* Support middlewares compiled to WASM (using extism)\n* Support Open Policy Agent policies for traffic control\n* Write your own custom middlewares\n * in scala deployed as jar files\n * in whatever language you want that can be compiled to WASM\n\n**Routes Monitoring**\n\n* Active healthchecks\n* Route state for the last 90 days\n* Calls tracing using W3C trace context\n* Export alerts and events to external database\n * file\n * S3\n * elastic\n * pulsar\n * kafka\n * webhook\n * mailer\n * logger\n* Real-time traffic metrics\n* Real-time traffic metrics (Datadog, Prometheus, StatsD)\n\n**Services discovery**\n\n* through DNS\n* through Eureka 2\n* through Kubernetes API\n* through custom otoroshi protocol\n\n**API security**\n\n* Access management with apikeys and quotas\n* Automatic apikeys secrets rotation\n* HTTPS and TLS\n* End-to-end mTLS calls \n* Routing constraints\n* Routing restrictions\n* JWT tokens validation and manipulation\n * can support multiple validator on the same routes\n\n**Administration UI**\n\n* Manage and organize all resources\n* Secured users access with Authentication module\n* Audited users actions\n* Dynamic changes at runtime without full reload\n* Test your routes without any external tools\n\n**Webapp authentication and security**\n\n* OAuth2.0/2.1 authentication\n* OpenID Connect (OIDC) authentication\n* LDAP authentication\n* JWT authentication\n* OAuth 1.0a authentication\n* SAML V2 authentication\n* Internal users management\n* Secret vaults support\n * Environment variables\n * Hashicorp Vault\n * Azure key vault\n * AWS secret manager\n * Google secret manager\n * Kubernetes secrets\n * Izanami\n * Spring Cloud Config\n * Http\n * Local\n\n**Certificates management**\n\n* Dynamic TLS certificates store \n* Dynamic TLS termination\n* Internal PKI\n * generate self signed certificates/CAs\n * generate/sign certificates/CAs/subCAs\n * AIA\n * OCSP responder\n * import P12/certificate bundles\n* ACME / Let's Encrypt support\n* On-the-fly certificate generation based on a CA certificate without request loss\n* JWKS exposition for public keypair\n* Default certificate\n* Customize mTLS trusted CAs in the TLS handshake\n\n**Clustering**\n\n* based on a control plane/data plane pattern\n* encrypted communication\n* backup capabilities to allow data plane to start without control plane reachable to improve resilience\n* relay routing to forward traffic from one network zone to others\n* distributed web authentication accross nodes\n\n**Performances and testing**\n\n* Chaos engineering\n* Horizontal Scalability or clustering\n* Canary testing\n* Http client in UI\n* Request debugging\n* Traffic capture\n\n**Kubernetes integration**\n\n* Standard Ingress controller\n* Custom Ingress controller\n * Manage Otoroshi resources from Kubernetes\n* Validation of resources via webhook\n* Service Mesh for easy service-to-service communication (based on Kubernetes sidecars)\n\n**Organize**\n\n* multi-organizations\n* multi-teams\n* routes groups\n\n**Developpers portal**\n\n* Using @link:[Daikoku](https://maif.github.io/daikoku/manual/index.html) { open=new }\n"},{"name":"getting-started.md","id":"/getting-started.md","url":"/getting-started.html","title":"Getting Started","content":"# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
\nRoute plugins:\nApikeys\n
\n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
\nRoute plugins:\nAuthentication\n
\n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial."},{"name":"communicate-with-kafka.md","id":"/how-to-s/communicate-with-kafka.md","url":"/how-to-s/communicate-with-kafka.html","title":"Communicate with Kafka","content":"# Communicate with Kafka\n\nEvery matching event can be sent to an [Apache Kafka topic](https://kafka.apache.org/).\n\n### SASL mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:SASL_PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n - KAFKA_CLIENT_USERS=user\n - KAFKA_CLIENT_PASSWORDS=password\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### PLAINTEXT mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SSL mechanism\n\n````bash\nwget https://raw.githubusercontent.com/confluentinc/confluent-platform-security-tools/master/kafka-generate-ssl.sh\n````\n\n````bash\nchmod +x kafka-generate-ssl.sh\n````\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"wurstmeister/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n\n kafka:\n image: wurstmeister/kafka:2.12-2.2.0\n depends_on:\n - zookeeper\n ports:\n - \"9092:9092\"\n environment:\n KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:9092'\n KAFKA_LISTENERS: 'SSL://0.0.0.0:9092'\n KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'\n KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_SSL_KEYSTORE_LOCATION: '/keystore/kafka.keystore.jks'\n KAFKA_SSL_KEYSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_KEY_PASSWORD: 'otoroshi'\n KAFKA_SSL_TRUSTSTORE_LOCATION: '/truststore/kafka.truststore.jks'\n KAFKA_SSL_TRUSTSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_SECURITY_INTER_BROKER_PROTOCOL: 'SSL'\n volumes:\n - ./truststore:/truststore\n - ./keystore:/keystore\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SASL_SSL mechanism\n\nGenerate the TLS certificates for the Kafka broker.\n\nCreate a file `generate.sh` with the following content and run the command\n\n````bash\nchmod +x generate.sh && ./generate.sh\n````\n\n````bash\n# Content of the generate.sh file\n\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚°\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚° cat generate.sh\n#!/usr/bin/env bash\n\nset -e\n\nKEYSTORE_FILENAME=\"kafka.keystore.jks\"\nVALIDITY_IN_DAYS=3650\nDEFAULT_TRUSTSTORE_FILENAME=\"kafka.truststore.jks\"\nTRUSTSTORE_WORKING_DIRECTORY=\"truststore\"\nKEYSTORE_WORKING_DIRECTORY=\"keystore\"\nCA_CERT_FILE=\"ca-cert\"\nKEYSTORE_SIGN_REQUEST=\"cert-file\"\nKEYSTORE_SIGN_REQUEST_SRL=\"ca-cert.srl\"\nKEYSTORE_SIGNED_CERT=\"cert-signed\"\n\nfunction file_exists_and_exit() {\n echo \"'$1' cannot exist. Move or delete it before\"\n echo \"re-running this script.\"\n exit 1\n}\n\nif [ -e \"$KEYSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $KEYSTORE_WORKING_DIRECTORY\nfi\n\nif [ -e \"$CA_CERT_FILE\" ]; then\n file_exists_and_exit $CA_CERT_FILE\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST_SRL\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST_SRL\nfi\n\nif [ -e \"$KEYSTORE_SIGNED_CERT\" ]; then\n file_exists_and_exit $KEYSTORE_SIGNED_CERT\nfi\n\necho\necho \"Welcome to the Kafka SSL keystore and truststore generator script.\"\n\necho\necho \"First, do you need to generate a trust store and associated private key,\"\necho \"or do you already have a trust store file and private key?\"\necho\necho -n \"Do you need to generate a trust store and associated private key? [yn] \"\nread generate_trust_store\n\ntrust_store_file=\"\"\ntrust_store_private_key_file=\"\"\n\nif [ \"$generate_trust_store\" == \"y\" ]; then\n if [ -e \"$TRUSTSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $TRUSTSTORE_WORKING_DIRECTORY\n fi\n\n mkdir $TRUSTSTORE_WORKING_DIRECTORY\n echo\n echo \"OK, we'll generate a trust store and associated private key.\"\n echo\n echo \"First, the private key.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - A password for the private key. Remember this.\"\n echo \" - Information about you and your company.\"\n echo \" - NOTE that the Common Name (CN) is currently not important.\"\n\n openssl req -new -x509 -keyout $TRUSTSTORE_WORKING_DIRECTORY/ca-key \\\n -out $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -days $VALIDITY_IN_DAYS\n\n trust_store_private_key_file=\"$TRUSTSTORE_WORKING_DIRECTORY/ca-key\"\n\n echo\n echo \"Two files were created:\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-key -- the private key used later to\"\n echo \" sign certificates\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -- the certificate that will be\"\n echo \" stored in the trust store in a moment and serve as the certificate\"\n echo \" authority (CA). Once this certificate has been stored in the trust\"\n echo \" store, it will be deleted. It can be retrieved from the trust store via:\"\n echo \" $ keytool -keystore -export -alias CARoot -rfc\"\n\n echo\n echo \"Now the trust store will be generated from the certificate.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - the trust store's password (labeled 'keystore'). Remember this\"\n echo \" - a confirmation that you want to import the certificate\"\n\n keytool -keystore $TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME \\\n -alias CARoot -import -file $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\n\n trust_store_file=\"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME\"\n\n echo\n echo \"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME was created.\"\n\n # don't need the cert because it's in the trust store.\n rm $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\nelse\n echo\n echo -n \"Enter the path of the trust store file. \"\n read -e trust_store_file\n\n if ! [ -f $trust_store_file ]; then\n echo \"$trust_store_file isn't a file. Exiting.\"\n exit 1\n fi\n\n echo -n \"Enter the path of the trust store's private key. \"\n read -e trust_store_private_key_file\n\n if ! [ -f $trust_store_private_key_file ]; then\n echo \"$trust_store_private_key_file isn't a file. Exiting.\"\n exit 1\n fi\nfi\n\necho\necho \"Continuing with:\"\necho \" - trust store file: $trust_store_file\"\necho \" - trust store private key: $trust_store_private_key_file\"\n\nmkdir $KEYSTORE_WORKING_DIRECTORY\n\necho\necho \"Now, a keystore will be generated. Each broker and logical client needs its own\"\necho \"keystore. This script will create only one keystore. Run this script multiple\"\necho \"times for multiple keystores.\"\necho\necho \"You will be prompted for the following:\"\necho \" - A keystore password. Remember it.\"\necho \" - Personal information, such as your name.\"\necho \" NOTE: currently in Kafka, the Common Name (CN) does not need to be the FQDN of\"\necho \" this host. However, at some point, this may change. As such, make the CN\"\necho \" the FQDN. Some operating systems call the CN prompt 'first / last name'\"\necho \" - A key password, for the key being generated within the keystore. Remember this.\"\n\n# To learn more about CNs and FQDNs, read:\n# https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedTrustManager.html\n\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME \\\n -alias localhost -validity $VALIDITY_IN_DAYS -genkey -keyalg RSA\n\necho\necho \"'$KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME' now contains a key pair and a\"\necho \"self-signed certificate. Again, this keystore can only be used for one broker or\"\necho \"one logical client. Other brokers or clients need to generate their own keystores.\"\n\necho\necho \"Fetching the certificate from the trust store and storing in $CA_CERT_FILE.\"\necho\necho \"You will be prompted for the trust store's password (labeled 'keystore')\"\n\nkeytool -keystore $trust_store_file -export -alias CARoot -rfc -file $CA_CERT_FILE\n\necho\necho \"Now a certificate signing request will be made to the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost \\\n -certreq -file $KEYSTORE_SIGN_REQUEST\n\necho\necho \"Now the trust store's private key (CA) will sign the keystore's certificate.\"\necho\necho \"You will be prompted for the trust store's private key password.\"\nopenssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \\\n -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \\\n -days $VALIDITY_IN_DAYS -CAcreateserial\n# creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed.\n\necho\necho \"Now the CA will be imported into the keystore.\"\necho\necho \"You will be prompted for the keystore's password and a confirmation that you want to\"\necho \"import the certificate.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias CARoot \\\n -import -file $CA_CERT_FILE\nrm $CA_CERT_FILE # delete the trust store cert because it's stored in the trust store.\n\necho\necho \"Now the keystore's signed certificate will be imported back into the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost -import \\\n -file $KEYSTORE_SIGNED_CERT\n\necho\necho \"All done!\"\necho\necho \"Delete intermediate files? They are:\"\necho \" - '$KEYSTORE_SIGN_REQUEST_SRL': CA serial number\"\necho \" - '$KEYSTORE_SIGN_REQUEST': the keystore's certificate signing request\"\necho \" (that was fulfilled)\"\necho \" - '$KEYSTORE_SIGNED_CERT': the keystore's certificate, signed by the CA, and stored back\"\necho \" into the keystore\"\necho -n \"Delete? [yn] \"\nread delete_intermediate_files\n\nif [ \"$delete_intermediate_files\" == \"y\" ]; then\n rm $KEYSTORE_SIGN_REQUEST_SRL\n rm $KEYSTORE_SIGN_REQUEST\n rm $KEYSTORE_SIGNED_CERT\nfi\n````\n\nCreate, in the same repository, a repository named `secrets` with the following configuration.\n\n````bash \n# Content of ~/tmp/kafka/secrets/kafka_server_jaas.conf\n\nClient {\n org.apache.kafka.common.security.plain.PlainLoginModule required\n username=\"user\"\n password=\"password\";\n};\n````\n\nCreate a `docker-compose.yml` file with the following content.\n\n````bash\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n````\n\nAt this point, your repository should be \n````\n/tmp/kafka\n | generate.sh\n | docker-compose.yml\n | truststore\n | kafka.truststore.jks\n | keystore \n | kafka.keystore.jks\n | secrets \n | kafka_server_jaas.conf\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"create-custom-auth-module.md","id":"/how-to-s/create-custom-auth-module.md","url":"/how-to-s/create-custom-auth-module.html","title":"Create your Authentication module","content":"# Create your Authentication module\n\nAuthentication modules can be used to protect routes. In some cases, you need to create your own custom authentication module to create a new one or simply inherit and extend an exiting module.\n\nYou can write your own authentication using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"my-custom-auth-module\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1x.x.x\"\n )\n```\n\nJust below, you can find an example of Custom Auth. module. \n\n```scala\npackage auth.custom\n\nimport akka.http.scaladsl.util.FastFuture\nimport otoroshi.auth.{AuthModule, AuthModuleConfig, Form, SessionCookieValues}\nimport otoroshi.controllers.routes\nimport otoroshi.env.Env\nimport otoroshi.models._\nimport otoroshi.security.IdGenerator\nimport otoroshi.utils.JsonPathValidator\nimport otoroshi.utils.syntax.implicits.BetterSyntax\nimport play.api.http.MimeTypes\nimport play.api.libs.json._\nimport play.api.mvc._\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\ncase class CustomModuleConfig(\n id: String,\n name: String,\n desc: String,\n clientSideSessionEnabled: Boolean,\n sessionMaxAge: Int = 86400,\n userValidators: Seq[JsonPathValidator] = Seq.empty,\n tags: Seq[String],\n metadata: Map[String, String],\n sessionCookieValues: SessionCookieValues,\n location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),\n form: Option[Form] = None,\n foo: String = \"bar\"\n ) extends AuthModuleConfig {\n def `type`: String = \"custom\"\n def humanName: String = \"Custom Authentication\"\n\n override def authModule(config: GlobalConfig): AuthModule = CustomAuthModule(this)\n override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)\n\n lazy val format = new Format[CustomModuleConfig] {\n override def writes(o: CustomModuleConfig): JsValue = o.asJson\n\n override def reads(json: JsValue): JsResult[CustomModuleConfig] = Try {\n CustomModuleConfig(\n location = otoroshi.models.EntityLocation.readFromKey(json),\n id = (json \\ \"id\").as[String],\n name = (json \\ \"name\").as[String],\n desc = (json \\ \"desc\").asOpt[String].getOrElse(\"--\"),\n clientSideSessionEnabled = (json \\ \"clientSideSessionEnabled\").asOpt[Boolean].getOrElse(true),\n sessionMaxAge = (json \\ \"sessionMaxAge\").asOpt[Int].getOrElse(86400),\n metadata = (json \\ \"metadata\").asOpt[Map[String, String]].getOrElse(Map.empty),\n tags = (json \\ \"tags\").asOpt[Seq[String]].getOrElse(Seq.empty[String]),\n sessionCookieValues =\n (json \\ \"sessionCookieValues\").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),\n userValidators = (json \\ \"userValidators\")\n .asOpt[Seq[JsValue]]\n .map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))\n .getOrElse(Seq.empty),\n form = (json \\ \"form\").asOpt[JsValue].flatMap(json => Form._fmt.reads(json) match {\n case JsSuccess(value, _) => Some(value)\n case JsError(_) => None\n }),\n foo = (json \\ \"foo\").asOpt[String].getOrElse(\"bar\")\n )\n } match {\n case Failure(exception) => JsError(exception.getMessage)\n case Success(value) => JsSuccess(value)\n }\n }.asInstanceOf[Format[AuthModuleConfig]]\n\n override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = format\n\n override def asJson =\n location.jsonWithKey ++ Json.obj(\n \"type\" -> \"custom\",\n \"id\" -> this.id,\n \"name\" -> this.name,\n \"desc\" -> this.desc,\n \"clientSideSessionEnabled\" -> this.clientSideSessionEnabled,\n \"sessionMaxAge\" -> this.sessionMaxAge,\n \"metadata\" -> this.metadata,\n \"tags\" -> JsArray(tags.map(JsString.apply)),\n \"sessionCookieValues\" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),\n \"userValidators\" -> JsArray(userValidators.map(_.json)),\n \"form\" -> this.form.map(Form._fmt.writes),\n \"foo\" -> foo\n )\n\n def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)\n\n override def cookieSuffix(desc: ServiceDescriptor) = s\"custom-auth-$id\"\n def theDescription: String = desc\n def theMetadata: Map[String, String] = metadata\n def theName: String = name\n def theTags: Seq[String] = tags\n}\n\nobject CustomAuthModule {\n def defaultConfig = CustomModuleConfig(\n id = IdGenerator.namedId(\"auth_mod\", IdGenerator.uuid),\n name = \"My custom auth. module\",\n desc = \"My custom auth. module\",\n tags = Seq.empty,\n metadata = Map.empty,\n sessionCookieValues = SessionCookieValues(),\n clientSideSessionEnabled = true,\n form = None)\n}\n\ncase class CustomAuthModule(authConfig: CustomModuleConfig) extends AuthModule {\n def this() = this(CustomAuthModule.defaultConfig)\n\n override def paLoginPage(request: RequestHeader, config: GlobalConfig, descriptor: ServiceDescriptor, isRoute: Boolean)\n (implicit ec: ExecutionContext, env: Env): Future[Result] = {\n val redirect = request.getQueryString(\"redirect\")\n val hash = env.sign(s\"${authConfig.id}:::${descriptor.id}\")\n env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>\n Results\n .Ok(auth.custom.views.html.login(s\"/privateapps/generic/callback?desc=${descriptor.id}&hash=$hash&route=${isRoute}\", token))\n .as(MimeTypes.HTML)\n .addingToSession(\n \"ref\" -> authConfig.id,\n s\"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}\" -> redirect.getOrElse(\n routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)(request)\n )\n )(request)\n .future\n }\n }\n\n override def paLogout(request: RequestHeader, user: Option[PrivateAppsUser], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = FastFuture.successful(Right(None))\n\n override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[String, PrivateAppsUser]] = {\n PrivateAppsUser(\n randomId = IdGenerator.token(64),\n name = \"foo\",\n email = s\"foo@oto.tools\",\n profile = Json.obj(\n \"name\" -> \"foo\",\n \"email\" -> s\"foo@oto.tools\"\n ),\n realm = authConfig.cookieSuffix(descriptor),\n otoroshiData = None,\n authConfigId = authConfig.id,\n tags = Seq.empty,\n metadata = Map.empty,\n location = authConfig.location\n )\n .validate(authConfig.userValidators)\n .vfuture\n }\n\n override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Result] = ???\n\n override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = ???\n\n override def boCallback(request: Request[AnyContent], config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[String, BackOfficeUser]] = ???\n}\n```\n\nThis custom Auth. module inherits from AuthModule (the Auth module have to inherit from the AuthModule trait to be found by Otoroshi). It exposes a simple UI to login, and create an user for each callback request without any verification. Methods starting with bo will be called in case that the auth. module is used on the back office and in other cases, the pa methods (pa for Private App) will be called to protect a route.\n\nThis custom Auth. module uses a [Play template](https://www.playframework.com/documentation/2.8.x/ScalaTemplates) to display the login page. It's not required by Otoroshi but it's a easy way to create a login form.\n\n```html \n@import otoroshi.env.Env\n\n@(action: String, token: String)\n\n
\n

Login page

\n\n
\n \n \n Login\n \n \n
\n```\n\nYour hierarchy files should be something like:\n\n```\nauth\n| custom\n |customModule.scala\n | views\n | login.scala.html\n```\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/customModule.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in the authentication modules, you can chose your custom module in the list."},{"name":"custom-initial-state.md","id":"/how-to-s/custom-initial-state.md","url":"/how-to-s/custom-initial-state.html","title":"Initial state customization","content":"# Initial state customization\n\nwhen you start otoroshi for the first time, some basic entities will be created and stored in the datastore in order to make your instance work properly. However it might not be enough for your use case but you do want to bother with restoring a complete otoroshi export.\n\nIn order to make state customization easy, otoroshi provides the config. key `otoroshi.initialCustomization`, overriden by the env. variable `OTOROSHI_INITIAL_CUSTOMIZATION`\n\nThe expected structure is the following :\n\n```javascript\n{\n \"config\": { ... },\n \"admins\": [],\n \"simpleAdmins\": [],\n \"serviceGroups\": [],\n \"apiKeys\": [],\n \"serviceDescriptors\": [],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [],\n \"teams\": []\n}\n```\n\nin this structure, everything is optional. For every array property, items will be added to the datastore. For the global config. object, you can just add the parts that you need, and they will be merged with the existing config. object of the datastore.\n\n## Customize the global config.\n\nfor instance, if you want to customize the behavior of the TLS termination, you can use the following :\n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"config\":{\"tlsSettings\":{\"defaultDomain\":\"www.foo.bar\",\"randomIfNotFound\":false}}'\n```\n\n## Customize entities\n\nif you want to add apikeys at first boot \n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"apikeys\":[{\"_loc\":{\"tenant\":\"default\",\"teams\":[\"default\"]},\"clientId\":\"ksVlQ2KlZm0CnDfP\",\"clientSecret\":\"usZYbE1iwSsbpKY45W8kdbZySj1M5CWvFXe0sPbZ0glw6JalMsgorDvSBdr2ZVBk\",\"clientName\":\"awesome-apikey\",\"description\":\"the awesome apikey\",\"authorizedGroup\":\"default\",\"authorizedEntities\":[\"group_default\"],\"enabled\":true,\"readOnly\":false,\"allowClientIdOnly\":false,\"throttlingQuota\":10000000,\"dailyQuota\":10000000,\"monthlyQuota\":10000000,\"constrainedServicesOnly\":false,\"restrictions\":{\"enabled\":false,\"allowLast\":true,\"allowed\":[],\"forbidden\":[],\"notFound\":[]},\"rotation\":{\"enabled\":false,\"rotationEvery\":744,\"gracePeriod\":168,\"nextSecret\":null},\"validUntil\":null,\"tags\":[],\"metadata\":{}}]}'\n```\n"},{"name":"custom-log-levels.md","id":"/how-to-s/custom-log-levels.md","url":"/how-to-s/custom-log-levels.html","title":"Log levels customization","content":"# Log levels customization\n\nIf you want to customize the log level of your otoroshi instances, it's pretty easy to do it using environment variables or configuration file.\n\n## Customize log level for one logger with configuration file\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare in your otoroshi configuration file\n\n```\notoroshi.loggers {\n ...\n otoroshi-http-handler = \"DEBUG\"\n ...\n}\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## Customize log level for one logger with environment variable\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare an environment variable named `OTOROSHI_LOGGERS_OTOROSHI_HTTP_HANDLER` with value `DEBUG`. The rule is \n\n```scala\n\"OTOROSHI_LOGGERS_\" + loggerName.toUpperCase().replace(\"-\", \"_\")\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## List of loggers\n\n* [`otoroshi-error-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-handler%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-http-handler-debug`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler-debug%22%29)\n* [`otoroshi-websocket-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler%22%29)\n* [`otoroshi-websocket`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket%22%29)\n* [`otoroshi-websocket-handler-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler-actor%22%29)\n* [`otoroshi-snowmonkey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-worker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-worker%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-auth-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-controller%22%29)\n* [`otoroshi-swagger-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-swagger-controller%22%29)\n* [`otoroshi-u2f-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-u2f-controller%22%29)\n* [`otoroshi-backoffice-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-backoffice-api%22%29)\n* [`otoroshi-health-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-api%22%29)\n* [`otoroshi-stats-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-stats-api%22%29)\n* [`otoroshi-admin-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-admin-api%22%29)\n* [`otoroshi-auth-modules-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-modules-api%22%29)\n* [`otoroshi-certificates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-api%22%29)\n* [`otoroshi-pki`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-pki%22%29)\n* [`otoroshi-scripts-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-scripts-api%22%29)\n* [`otoroshi-analytics-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-api%22%29)\n* [`otoroshi-import-export-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-import-export-api%22%29)\n* [`otoroshi-templates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-templates-api%22%29)\n* [`otoroshi-teams-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-teams-api%22%29)\n* [`otoroshi-events-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-api%22%29)\n* [`otoroshi-canary-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary-api%22%29)\n* [`otoroshi-data-exporter-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-api%22%29)\n* [`otoroshi-services-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-services-api%22%29)\n* [`otoroshi-tcp-service-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-service-api%22%29)\n* [`otoroshi-tenants-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tenants-api%22%29)\n* [`otoroshi-global-config-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config-api%22%29)\n* [`otoroshi-apikeys-fs-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fs-api%22%29)\n* [`otoroshi-apikeys-fg-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fg-api%22%29)\n* [`otoroshi-apikeys-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-api%22%29)\n* [`otoroshi-statsd-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-statsd-actor%22%29)\n* [`otoroshi-snow-monkey-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snow-monkey-api%22%29)\n* [`otoroshi-jobs-eventstore-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-eventstore-checker%22%29)\n* [`otoroshi-initials-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-initials-certs-job%22%29)\n* [`otoroshi-alert-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor%22%29)\n* [`otoroshi-alert-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor-supervizer%22%29)\n* [`otoroshi-alerts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alerts%22%29)\n* [`otoroshi-apikeys-secrets-rotation-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-secrets-rotation-job%22%29)\n* [`otoroshi-loader`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-loader%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-analytics-writes-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-writes-elastic%22%29)\n* [`otoroshi-analytics-reads-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-reads-elastic%22%29)\n* [`otoroshi-events-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-actor-supervizer%22%29)\n* [`otoroshi-data-exporter`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter%22%29)\n* [`otoroshi-data-exporter-update-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-update-job%22%29)\n* [`otoroshi-kafka-wrapper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-wrapper%22%29)\n* [`otoroshi-kafka-connector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-connector%22%29)\n* [`otoroshi-analytics-webhook`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-webhook%22%29)\n* [`otoroshi-jobs-software-updates`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-software-updates%22%29)\n* [`otoroshi-analytics-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor%22%29)\n* [`otoroshi-analytics-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor-supervizer%22%29)\n* [`otoroshi-analytics-event`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-event%22%29)\n* [`otoroshi-env`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-env%22%29)\n* [`otoroshi-script-compiler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-compiler%22%29)\n* [`otoroshi-script-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-manager%22%29)\n* [`otoroshi-script`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-custom-timeouts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-custom-timeouts%22%29)\n* [`otoroshi-client-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-config%22%29)\n* [`otoroshi-canary`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary%22%29)\n* [`otoroshi-redirection-settings`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redirection-settings%22%29)\n* [`otoroshi-service-descriptor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor%22%29)\n* [`otoroshi-service-descriptor-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor-datastore%22%29)\n* [`otoroshi-console-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-console-mailer%22%29)\n* [`otoroshi-mailgun-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailgun-mailer%22%29)\n* [`otoroshi-mailjet-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailjet-mailer%22%29)\n* [`otoroshi-sendgrid-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sendgrid-mailer%22%29)\n* [`otoroshi-generic-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-generic-mailer%22%29)\n* [`otoroshi-clevercloud-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-clevercloud-client%22%29)\n* [`otoroshi-metrics`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-metrics%22%29)\n* [`otoroshi-gzip-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-gzip-config%22%29)\n* [`otoroshi-regex-pool`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-regex-pool%22%29)\n* [`otoroshi-ws-client-chooser`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ws-client-chooser%22%29)\n* [`otoroshi-akka-ws-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-akka-ws-client%22%29)\n* [`otoroshi-http-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-implicits%22%29)\n* [`otoroshi-service-group`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-group%22%29)\n* [`otoroshi-data-exporter-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config%22%29)\n* [`otoroshi-data-exporter-config-migration-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config-migration-job%22%29)\n* [`otoroshi-lets-encrypt-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lets-encrypt-helper%22%29)\n* [`otoroshi-apkikey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apkikey%22%29)\n* [`otoroshi-error-template`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-template%22%29)\n* [`otoroshi-job-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-job-manager%22%29)\n* [`otoroshi-plugins-internal-eventlistener-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-internal-eventlistener-actor%22%29)\n* [`otoroshi-global-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config%22%29)\n* [`otoroshi-jwks`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwks%22%29)\n* [`otoroshi-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwt-verifier%22%29)\n* [`otoroshi-global-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-jwt-verifier%22%29)\n* [`otoroshi-snowmonkey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey-config%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-service-datatstore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-datatstore%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-redis-like-store`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-like-store%22%29)\n* [`otoroshi-globalconfig-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-globalconfig-datastore%22%29)\n* [`otoroshi-reactive-pg-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-datastores%22%29)\n* [`otoroshi-reactive-pg-kv`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-kv%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-apikey-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikey-datastore%22%29)\n* [`otoroshi-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastore%22%29)\n* [`otoroshi-certificate-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificate-datastore%22%29)\n* [`otoroshi-simple-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-simple-admin-datastore%22%29)\n* [`otoroshi-atomic-in-memory-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-atomic-in-memory-datastore%22%29)\n* [`otoroshi-lettuce-redis`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis%22%29)\n* [`otoroshi-lettuce-redis-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis-cluster%22%29)\n* [`otoroshi-redis-lettuce-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-lettuce-datastores%22%29)\n* [`otoroshi-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastores%22%29)\n* [`otoroshi-file-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-file-db-datastores%22%29)\n* [`otoroshi-http-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-db-datastores%22%29)\n* [`otoroshi-s3-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-s3-datastores%22%29)\n* [`PluginDocumentationGenerator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22PluginDocumentationGenerator%22%29)\n* [`otoroshi-health-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-checker%22%29)\n* [`otoroshi-healthcheck-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-job%22%29)\n* [`otoroshi-healthcheck-local-cache-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-local-cache-job%22%29)\n* [`otoroshi-plugins-response-cache`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-response-cache%22%29)\n* [`otoroshi-oidc-apikey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-oidc-apikey-config%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-info%22%29)\n* [`otoroshi-plugins-ipstack-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-ipstack-geolocation-info%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-helper%22%29)\n* [`otoroshi-plugins-user-agent-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-helper%22%29)\n* [`otoroshi-plugins-user-agent-extractor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-extractor%22%29)\n* [`otoroshi-global-el`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-el%22%29)\n* [`otoroshi-plugins-oauth1-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-oauth1-caller-plugin%22%29)\n* [`otoroshi-dynamic-sslcontext`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-dynamic-sslcontext%22%29)\n* [`otoroshi-plugins-access-log-clf`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-clf%22%29)\n* [`otoroshi-plugins-access-log-json`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-json%22%29)\n* [`otoroshi-plugins-kafka-access-log`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kafka-access-log%22%29)\n* [`otoroshi-plugins-kubernetes-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-client%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-sync%22%29)\n* [`otoroshi-plugins-kubernetes-crds-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-crds-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-sync%22%29)\n* [`otoroshi-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cluster%22%29)\n* [`otoroshi-crd-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-crd-validator%22%29)\n* [`otoroshi-sidecar-injector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sidecar-injector%22%29)\n* [`otoroshi-plugins-kubernetes-cert-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-cert-sync%22%29)\n* [`otoroshi-plugins-kubernetes-to-otoroshi-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-to-otoroshi-certs-job%22%29)\n* [`otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job%22%29)\n* [`otoroshi-apikeys-workflow-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-workflow-job%22%29)\n* [`otoroshi-cert-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-helper%22%29)\n* [`otoroshi-certificates-ocsp`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-ocsp%22%29)\n* [`otoroshi-claim`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-claim%22%29)\n* [`otoroshi-cert`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert%22%29)\n* [`otoroshi-ssl-provider`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-provider%22%29)\n* [`otoroshi-cert-data`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-data%22%29)\n* [`otoroshi-client-cert-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-cert-validator%22%29)\n* [`otoroshi-ssl-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-implicits%22%29)\n* [`otoroshi-saml-validator-utils`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-saml-validator-utils%22%29)\n* [`otoroshi-global-saml-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-saml-config%22%29)\n* [`otoroshi-plugins-hmac-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-caller-plugin%22%29)\n* [`otoroshi-plugins-hmac-access-validator-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-access-validator-plugin%22%29)\n* [`otoroshi-plugins-hasallowedusersvalidator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hasallowedusersvalidator%22%29)\n* [`otoroshi-auth-module-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-module-config%22%29)\n* [`otoroshi-basic-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-basic-auth-config%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n* [`otoroshi-plugins-jsonpath-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-jsonpath-helper%22%29)\n* [`otoroshi-global-oauth2-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-config%22%29)\n* [`otoroshi-global-oauth2-module`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-module%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n"},{"name":"end-to-end-mtls.md","id":"/how-to-s/end-to-end-mtls.md","url":"/how-to-s/end-to-end-mtls.html","title":"End-to-end mTLS","content":"# End-to-end mTLS\n\nIf you want to use MTLS on otoroshi, you first need to enable it. It is not enabled by default as it will make TLS handshake way heavier. \nTo enable it just change the following config :\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\nYou can use the `Want` setup if you cant to have both mtls on some services and no mtls on other services.\n\nYou can also change the trusted CA list sent in the handshake certificate request from the `Danger Zone` in `Tls Settings`.\n\nOtoroshi support mutual TLS out of the box. mTLS from client to Otoroshi and from Otoroshi to targets are supported. In this article we will see how to configure Otoroshi to use end-to-end mTLS. All code and files used in this articles can be found on the [Otoroshi github](https://github.com/MAIF/otoroshi/tree/master/demos/mtls)\n\n### Create certificates\n\nBut first we need to generate some certificates to make the demo work\n\n```sh\nmkdir mtls-demo\ncd mtls-demo\nmkdir ca\nmkdir server\nmkdir client\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-backend.key 4096\n# remove pass phrase\nopenssl rsa -in ./ca/ca-backend.key -out ./ca/ca-backend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-backend.key -out ./ca/ca-backend.cer -subj \"/CN=MTLSB\"\n\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-frontend.key 2048\n# remove pass phrase\nopenssl rsa -in ./ca/ca-frontend.key -out ./ca/ca-frontend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-frontend.key -out ./ca/ca-frontend.cer -subj \"/CN=MTLSF\"\n\n\n# now create the backend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.backend.oto.tools.key -out ./server/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.backend.oto.tools.key -sha256 -out ./server/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 1 -out ./server/_.backend.oto.tools.cer\n# verify the certificate, should output './server/_.backend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-backend.cer ./server/_.backend.oto.tools.cer\n\n\n# now create the frontend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.frontend.oto.tools.key -out ./server/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.frontend.oto.tools.key -sha256 -out ./server/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 1 -out ./server/_.frontend.oto.tools.cer\n# verify the certificate, should output './server/_.frontend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.cer\n\n\n# now create the client cert key for backend, use password as pass phrase\nopenssl genrsa -out ./client/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 2 -out ./client/_.backend.oto.tools.cer\n# generate a pem version of the cert and key, use password as password\nopenssl x509 -in client/_.backend.oto.tools.cer -out client/_.backend.oto.tools.pem -outform PEM\n\n\n# now create the client cert key for frontend, use password as pass phrase\nopenssl genrsa -out ./client/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 2 -out ./client/_.frontend.oto.tools.cer\n# generate a pkcs12 version of the cert and key, use password as password\n# openssl pkcs12 -export -clcerts -in client/_.frontend.oto.tools.cer -inkey client/_.frontend.oto.tools.key -out client/_.frontend.oto.tools.p12\nopenssl x509 -in client/_.frontend.oto.tools.cer -out client/_.frontend.oto.tools.pem -outform PEM\n```\n\nOnce it's done, you should have something like\n\n```sh\n$ tree\n.\n├── backend.js\n├── ca\n│   ├── ca-backend.cer\n│   ├── ca-backend.key\n│   ├── ca-frontend.cer\n│   └── ca-frontend.key\n├── client\n│   ├── _.backend.oto.tools.cer\n│   ├── _.backend.oto.tools.csr\n│   ├── _.backend.oto.tools.key\n│   ├── _.backend.oto.tools.pem\n│   ├── _.frontend.oto.tools.cer\n│   ├── _.frontend.oto.tools.csr\n│   ├── _.frontend.oto.tools.key\n│   └── _.frontend.oto.tools.pem\n└── server\n ├── _.backend.oto.tools.cer\n ├── _.backend.oto.tools.csr\n ├── _.backend.oto.tools.key\n ├── _.frontend.oto.tools.cer\n ├── _.frontend.oto.tools.csr\n └── _.frontend.oto.tools.key\n\n3 directories, 18 files\n```\n\n### The backend service \n\nnow, let's create a backend service using nodejs. Create a file named `backend.js`\n\n```sh\ntouch backend.js\n```\n\nand put the following content\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n}; \n\nconst server = https.createServer(options, (req, res) => { \n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nto run the server, just do \n\n```sh\nnode ./backend.js\n```\n\nnow you can try your server with\n\n```sh\ncurl --cacert ./ca/ca-backend.cer 'https://api.backend.oto.tools:8444/'\n```\n\nThis should output :\n```json\n{ \"message\": \"Hello World!\" }\n```\n\nnow modify your backend server to ensure that the client provides a client certificate like:\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n requestCert: true, \n rejectUnauthorized: true\n}; \n\nconst server = https.createServer(options, (req, res) => { \n console.log('Client certificate CN: ', req.socket.getPeerCertificate().subject.CN);\n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nyou can test your new server with\n\n```sh\ncurl \\\n --cacert ./ca/ca-backend.cer \\\n --cert ./client/_.backend.oto.tools.pem \\\n --key ./client/_.backend.oto.tools.key 'https://api.backend.oto.tools:8444/'\n```\n\nthe output should be :\n\n```json\n{ \"message\": \"Hello World!\" }\n```\n\n### Otoroshi setup\n\nDownload the latest version of the Otoroshi jar and run it like\n\n```sh\n java \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.ssl.fromOutside.clientAuth=Want \\\n -jar -Dotoroshi.storage=file otoroshi.jar\n\n[info] otoroshi-env - Admin API exposed on http://otoroshi-api.oto.tools:8080\n[info] otoroshi-env - Admin UI exposed on http://otoroshi.oto.tools:8080\n[info] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[info] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[info] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / password\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n[info] p.c.s.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:8443\n[info] otoroshi-env - Generating a self signed SSL certificate for https://*.oto.tools ...\n```\n\nand log into otoroshi with the tuple `admin@otoroshi.io / password` displayed in the logs. \n\nOnce logged in, navigate to the routes page and create a new route.\n\n* Set a name then validate the creation\n* On frontend node, add `api.frontend.oto.tools` in the list of domains\n* On backend node, replace the target with `api.backend.oto.tools` as hostname and `8444` as port. \n\nSave the route and try to call it.\n\n```sh\ncurl 'http://api.frontend.oto.tools:8080/'\n```\n\nThis should output :\n```json\n{\"Otoroshi-Error\": \"Something went wrong, you should try later. Thanks for your understanding.\"}\n```\n\nyou should get an error due to the fact that Otoroshi doesn't know about the server certificate and the client certificate expected by the server.\n\nWe must declare the client and server certificates for `https://api.backend.oto.tools` to Otoroshi. \n\nGo to the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) and create a new item. Drag and drop the content of the `./client/_.backend.oto.tools.cer` and `./client/_.backend.oto.tools.key` files, respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you prefer to use the API, you can create an Otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.backend.oto.tools.cer ./ca/ca-backend.cer ./server/_.backend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n --data-binary @- \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle \n```\n\nnow we have to expose `https://api.frontend.oto.tools:8443` using otoroshi. \n\nCreate a second item. Copy and paste the content of `./server/_.frontend.oto.tools.cer` and `./server/_.frontend.oto.tools.key` respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you don't want to bother with UI copy/paste, you can use the import bundle api endpoint to create an otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.frontend.oto.tools.cer ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n --data-binary @- \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle\n```\n\nOnce created, go back to your route. On the target of the backend node, we have to enable the custom Otoroshi TLS.\n\n* Click on the backend node\n* Click on your target\n* Click on the Show advanced settings button\n* Click on Custom TLS setup\n* Enable the section\n* In the list of certificates, select the backend certificate\n* In the list of trusted certificates, select the frontend certificate\n* Save your route\n \nTry the following command\n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\nNow we want to enforce the fact that we want client certificate for `api.frontend.oto.tools`. \n\nSearch in the list of plugins and add the `Client Certificate Only` plugin to your route.\n\nnow if you retry \n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nyou should get an error because no client certificate is passed with the request. But if you pass the `./client/_.frontend.oto.tools.csr` client cert and the key in your curl call\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n### Client certificate matching plugin\n\nOtoroshi can restrict and check all incoming client certificates on a route.\n\nSearch in the list of plugins the `Client certificate matching` plugin and add it the the flow.\n\nSave the route and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nOur client certificate is not matched by Otoroshi. We have to add the subject DN in the configuration of the `Client certificate matching` plugin to authorize it.\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [],\n \"subjectDNs\": [\n \"CN=*.frontend.oto.tools\"\n ],\n \"issuerDNs\": [],\n \"regexSubjectDNs\": [],\n \"regexIssuerDNs\": []\n }\n}\n```\n\nSave the service and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n\n"},{"name":"export-alerts-using-mailgun.md","id":"/how-to-s/export-alerts-using-mailgun.md","url":"/how-to-s/export-alerts-using-mailgun.html","title":"Send alerts using mailgun","content":"# Send alerts using mailgun\n\nAll Otoroshi alerts can be send on different channels.\nOne of the ways is to send a group of specific alerts via emails.\n\nTo enable this behaviour, let's start by create an exporter of events.\n\nIn this tutorial, we will admit that you already have a mailgun account with an API key and a domain.\n\n## Create an Mailgun exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `mailer` in the `type` selector field\n6. Jump to `Exporter config` and select the `Mailgun` option\n7. Set the following values:\n* `EU` : false/true depending on your mailgun configuratin\n* `Mailgun api key` : your-mailgun-api-key\n* `Mailgun domain` : your-mailgun-domain\n* `Email addresses` : list of the recipient adresses\n\nWith this configuration, all Otoroshi events will be send to your listed addresses (we don't recommended to do that).\n\nTo filter events on `Alerts` type, we need to add the following configuration inside the `Filtering and projection` section (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)).\n\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n``` \n\nSave at the bottom page and enable the exporter (on the top of the page or in list of exporters). We will need to wait few seconds to receive the first alerts.\n\nThe **projection** field can be useful in the case you want to filter the fields contained in each alert sent.\n\nThe `Projection` field is a json where you can list the fields to keep for each alert.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be sent to the addresses of your recipients."},{"name":"export-events-to-elastic.md","id":"/how-to-s/export-events-to-elastic.md","url":"/how-to-s/export-events-to-elastic.html","title":"Export events to Elasticsearch","content":"# Export events to Elasticsearch\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Deploy a Elasticsearch and kibana stack on Docker\n\nLet's start by creating an Elasticsearch and Kibana stack on our machine (if it's already done for you, you can skip this section).\n\nTo start an Elasticsearch container for development or testing, run:\n\n```sh\ndocker network create elastic\ndocker pull docker.elastic.co/elasticsearch/elasticsearch:7.15.1\ndocker run --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e \"discovery.type=single-node\" docker.elastic.co/elasticsearch/elasticsearch:7.15.1\n```\n\n```sh\ndocker pull docker.elastic.co/kibana/kibana:7.15.1\ndocker run --name kib01-test --net elastic -p 5601:5601 -e \"ELASTICSEARCH_HOSTS=http://es01-test:9200\" docker.elastic.co/kibana/kibana:7.15.1\n```\n\nTo access Kibana, go to @link:[http://localhost:5601](http://localhost:5601) { open=new }.\n\n### Create an Elasticsearch exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `elastic` in the `type` selector field\n6. Jump to `Exporter config`\n7. Set the following values: `Cluster URI` -> `http://localhost:9200`\n\nThen test your configuration by clicking on the `Check connection` button. This should output a modal with the Elasticsearch version and the number of loaded docs.\n\nSave at the bottom of the page and enable the exporter (on the top of the page or in list of exporters).\n\n### Testing your configuration\n\nOne simple way to test is to setup the reading of our Elasticsearch instance by Otoroshi.\n\nNavigate to the danger zone (click on the cog on the top right and scroll to `danger zone`). Jump to the `Analytics: Elastic dashboard datasource (read)` section.\n\nSet the following values : `Cluster URI` -> `http://localhost:9200`\n\nThen click on the `Check connection`. This should ouput the same result as the previous part. Save the global configuration and navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/stats](http://otoroshi.oto.tools:8080/bo/dashboard/stats) { open=new }.\n\nThis should output a list of graphs.\n\n### Advanced usage\n\nBy default, an exporter handle all events from Otoroshi. In some case, you need to filter the events to send to elasticsearch.\n\nTo filter the events, jump to the `Filtering and projection` field in exporter view. Otoroshi supports to include a kind of events or to exclude a list of events (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)). \n\nAn example which keep only events with a field `@type` of value `AlertEvent`:\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n```\nAn example which exclude only events with a field `@type` of value `GatewayEvent` :\n```json\n{\n \"exclude\": [\n { \"@type\": \"GatewayEvent\" }\n ],\n \"include\": []\n}\n```\n\nThe next field is the **Projection**. This field is a json when you can list the fields to keep for each event.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be send to ES.\n\n### Debug your configuration\n\n#### Missing user rights on Elasticsearch\n\nWhen creating an exporter, Otoroshi try to join the index route of the elasticsearch instance. If you have a specific management access rights on Elasticsearch, you have two possiblities :\n\n- set a full access to the user used in Otoroshi for write in Elasticsearch\n- set the version of Elasticsearch inside the `Version` field of your exporter.\n\n#### None event appear in your Elasticsearch\n\nWhen creating an exporter, Otoroshi try to push the index template on Elasticsearch. If the post failed, Otoroshi will fail for each push of events and your database will keep empty. \n\nTo fix this problem, you can try to send the index template with the `Manually apply index template` button in your exporter."},{"name":"import-export-otoroshi-datastore.md","id":"/how-to-s/import-export-otoroshi-datastore.md","url":"/how-to-s/import-export-otoroshi-datastore.html","title":"Import and export Otoroshi datastore","content":"# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`"},{"name":"index.md","id":"/how-to-s/index.md","url":"/how-to-s/index.html","title":"How to's","content":"# How to's\n\nin this section, we will explain some mainstream Otoroshi usage scenario's \n\n* @ref:[Otoroshi and WASM](./wasm-usage.md)\n* @ref:[Wasmo](./wasmo-installation.md)\n* @ref:[Tailscale integration](./tailscale-integration.md)\n* @ref:[End-to-end mTLS](./end-to-end-mtls.md)\n* @ref:[Send alerts by emails](./export-alerts-using-mailgun.md)\n* @ref:[Export events to Elasticsearch](./export-events-to-elastic.md)\n* @ref:[Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* @ref:[Secure an app with Auth0](./secure-app-with-auth0.md)\n* @ref:[Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* @ref:[Secure an app with LDAP](./secure-app-with-ldap.md)\n* @ref:[Secure an api with apikeys](./secure-with-apikey.md)\n* @ref:[Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* @ref:[Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* @ref:[Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* @ref:[TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* @ref:[Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* @ref:[Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* @ref:[TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* @ref:[The resources loader](./resources-loader.md)\n* @ref:[Log levels customization](./custom-log-levels.md)\n* @ref:[Initial state customization](./custom-initial-state.md)\n* @ref:[Communicate with Kafka](./communicate-with-kafka.md)\n* @ref:[Create your custom Authentication module](./create-custom-auth-module.md)\n* @ref:[Working with Eureka](./working-with-eureka.md)\n* @ref:[Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* @ref:[Quickly expose a website and static files](./zip-backend-plugin.md)\n\n@@@ index\n\n\n* [WASM usage](./wasm-usage.md)\n* [wasmo](./wasmo-installation.md)\n* [Tailscale integration](./tailscale-integration.md)\n* [End-to-end mTLS](./end-to-end-mtls.md)\n* [Send alerts by emails](./export-alerts-using-mailgun.md)\n* [Export events to Elasticsearch](./export-events-to-elastic.md)\n* [Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* [Secure an app with Auth0](./secure-app-with-auth0.md)\n* [Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* [Secure an app with LDAP](./secure-app-with-ldap.md)\n* [Secure an api with apikeys](./secure-with-apikey.md)\n* [Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* [Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* [Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* [TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* [Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* [Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* [TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* [The resources loader](./resources-loader.md)\n* [Log levels customization](./custom-log-levels.md)\n* [Initial state customization](./custom-initial-state.md)\n* [Communicate with Kafka](./communicate-with-kafka.md)\n* [Create your custom Authentication module](./create-custom-auth-module.md)\n* [Working with Eureka](./working-with-eureka.md)\n* [Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* [Zip Backend plugin](./zip-backend-plugin.md) \n@@@\n"},{"name":"instantiate-waf-coraza.md","id":"/how-to-s/instantiate-waf-coraza.md","url":"/how-to-s/instantiate-waf-coraza.html","title":"Instantiate a WAF with Coraza","content":"# Instantiate a WAF with Coraza\n\n
\nRoute plugins:\nCoraza WAF\nOverride Host Header\n
\n\nSometimes you may want to secure an app with a [Web Appplication Firewall (WAF)](https://en.wikipedia.org/wiki/Web_application_firewall) and apply the security rules from the [OWASP Core Rule Set](https://owasp.org/www-project-modsecurity-core-rule-set/). To allow that, we integrated [the Coraza WAF](https://coraza.io/) in Otoroshi through a plugin that uses the WASM version of Coraza.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a WAF configuration\n\nfirst, go on [the features page of otoroshi](http://otoroshi.oto.tools:8080/bo/dashboard/features) and then click on the [Coraza WAF configs. item](http://otoroshi.oto.tools:8080/bo/dashboard/extensions/coraza-waf/coraza-configs). \n\nNow create a new configuration, give it a name and a description, ensure that you enabled the `Inspect req/res body` flag and save your configuration.\n\nThe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Configure Coraza and the OWASP Core Rule Set\n\nNow you can easily configure the coraza WAF in the `json` config. section. By default it should look something like :\n\n```json\n{\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n}\n```\n\nYou can find anything about it in [the documentation of Coraza](https://coraza.io/docs/tutorials/introduction/).\n\nhere we have the basic setup to apply the OWASP core rule set in detection mode only. \nSo each time Coraza will find something weird in a request, it will only log it but let the request pass.\n We can enable blocking by setting `\"SecRuleEngine On\"`\n\nwe can also deny access to the `/admin` uri by adding the following directive\n\n```json\n\"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\"\n```\n\nYou can also provide multiple profile of rules in the `directives_map` with different names and use the `per_authority_directives` object to map hostnames to a specific profile.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs/coraza-waf-demo' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\",\n \"SecRuleEngine On\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Add the WAF plugin on your route\n\nNow you can create a new route that will use your WAF configuration. Let say we want a route on `http://wouf.oto.tools:8080` to goes to `https://www.otoroshi.io`. Now add the `Coraza WAF` plugin to your route and in the configuration select the configuration you created previously.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n -H 'Content-Type: application/json' -d '\n{\n \"id\": \"route_demo\",\n \"name\": \"WAF route\",\n \"description\": \"A new route with a WAF enabled\",\n \"frontend\": {\n \"domains\": [\n \"wouf.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"www.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.wasm.proxywasm.NgCorazaWAF\",\n \"config\": {\n \"ref\": \"coraza-waf-demo\"\n },\n \"plugin_index\": {\n \"validate_access\": 0,\n \"transform_request\": 0,\n \"transform_response\": 0\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"plugin_index\": {\n \"transform_request\": 1\n }\n }\n ]\n}'\n```\n\n### Try to use an exploit ;)\n\nlet try to trigger Coraza with a Log4Shell crafted request:\n\n```sh\ncurl 'http://wouf.oto.tools:9999' -H 'foo: ${jndi:rmi://foo/bar}' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nor access to `/admin`\n\n```sh\ncurl 'http://wouf.oto.tools:9999/admin' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nif you look at otoroshi logs you will find something like :\n\n```log\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell \n [file \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\"] [line \"10608\"] [id \"944150\"] [rev \"\"] \n [msg \"Potential Remote Command Execution: Log4j / Log4shell\"] [data \"\"] [severity \"critical\"] \n [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] [tag \"application-multi\"] \n [tag \"language-java\"] [tag \"platform-multi\"] [tag \"attack-rce\"] [tag \"OWASP_CRS\"] \n [tag \"capec/1000/152/137/6\"] [tag \"PCI/6.5.2\"] [tag \"paranoia-level/1\"] [hostname \"wwwwouf.oto.tools\"] \n [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Inbound Anomaly Score Exceeded (Total Score: 5) \n [file \"@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf\"] [line \"11029\"] [id \"949110\"] [rev \"\"] \n [msg \"Inbound Anomaly Score Exceeded (Total Score: 5)\"] \n [data \"\"] [severity \"emergency\"] [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] \n [tag \"anomaly-evaluation\"] [hostname \"wwwwouf.oto.tools\"] [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"uTYakrlgMBydVGLodbz\" context_id=3 action=\"deny\" phase=\"http_response_headers\"\n...\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. [file \"\"] [line \"12914\"] \n [id \"101\"] [rev \"\"] [msg \"\"] [data \"\"] [severity \"emergency\"] [ver \"\"] [maturity \"0\"] [accuracy \"0\"] \n [hostname \"wwwwouf.oto.tools\"] [uri \"/admin\"] [unique_id \"mqXZeMdzRaVAqIiqvHf\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"mqXZeMdzRaVAqIiqvHf\" context_id=2 action=\"deny\" phase=\"http_request_headers\"\n```\n\n### Generated events\n\neach time Coraza will generate log about vunerability detection, an event will be generated in otoroshi and exporter through the usual data exporter way. The event will look like :\n\n```json\n{\n \"@id\" : \"86b647450-3cc7-42a9-aaec-828d261a8c74\",\n \"@timestamp\" : 1684938211157,\n \"@type\" : \"CorazaTrailEvent\",\n \"@product\" : \"otoroshi\",\n \"@serviceId\" : \"--\",\n \"@service\" : \"--\",\n \"@env\" : \"prod\",\n \"level\" : \"ERROR\",\n \"msg\" : \"Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell\",\n \"fields\" : {\n \"hostname\" : \"wouf.oto.tools\",\n \"maturity\" : \"0\",\n \"line\" : \"10608\",\n \"unique_id\" : \"oNbisKlXWaCdXntaUpq\",\n \"tag\" : \"paranoia-level/1\",\n \"data\" : \"\",\n \"accuracy\" : \"0\",\n \"uri\" : \"/\",\n \"rev\" : \"\",\n \"id\" : \"944150\",\n \"client\" : \"127.0.0.1\",\n \"ver\" : \"OWASP_CRS/4.0.0-rc1\",\n \"file\" : \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\",\n \"msg\" : \"Potential Remote Command Execution: Log4j / Log4shell\",\n \"severity\" : \"critical\"\n },\n \"raw\" : \"[client \\\"127.0.0.1\\\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell [file \\\"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\\\"] [line \\\"10608\\\"] [id \\\"944150\\\"] [rev \\\"\\\"] [msg \\\"Potential Remote Command Execution: Log4j / Log4shell\\\"] [data \\\"\\\"] [severity \\\"critical\\\"] [ver \\\"OWASP_CRS/4.0.0-rc1\\\"] [maturity \\\"0\\\"] [accuracy \\\"0\\\"] [tag \\\"application-multi\\\"] [tag \\\"language-java\\\"] [tag \\\"platform-multi\\\"] [tag \\\"attack-rce\\\"] [tag \\\"OWASP_CRS\\\"] [tag \\\"capec/1000/152/137/6\\\"] [tag \\\"PCI/6.5.2\\\"] [tag \\\"paranoia-level/1\\\"] [hostname \\\"wouf.oto.tools\\\"] [uri \\\"/\\\"] [unique_id \\\"oNbisKlXWaCdXntaUpq\\\"]\\n\",\n}\n```"},{"name":"resources-loader.md","id":"/how-to-s/resources-loader.md","url":"/how-to-s/resources-loader.html","title":"The resources loader","content":"# The resources loader\n\nThe resources loader is a tool to create an Otoroshi resource from a raw content. This content can be found on each Otoroshi resources pages (services descriptors, apikeys, certificates, etc ...). To get the content of a resource as file, you can use the two export buttons, one to export as JSON format and the other as YAML format.\n\nOnce exported, the content of the resource can be import with the resource loader. You can import single or multiples resources on one time, as JSON and YAML format.\n\nThe resource loader is available on this route [`bo/dashboard/resources-loader`](http://otoroshi.oto.tools:8080/bo/dashboard/resources-loader).\n\nOn this page, you can paste the content of your resources and click on **Load resources**.\n\nFor each detected resource, the loader will display :\n\n* a resource name corresponding to the field `name` \n* a resource type corresponding to the type of created resource (ServiceDescriptor, ApiKey, Certificate, etc)\n* a toggle to choose if you want to include the element for the creation step\n* the updated status by the creation process\n\nOnce you have selected the resources to create, you can **Import selected resources**.\n\nOnce generated, all status will be updated. If all is working, the status will be equals to done.\n\nIf you want to get back to the initial page, you can use the **restart** button."},{"name":"secure-an-app-with-jwt-verifiers.md","id":"/how-to-s/secure-an-app-with-jwt-verifiers.md","url":"/how-to-s/secure-an-app-with-jwt-verifiers.html","title":"Secure an api with jwt verifiers","content":"# Secure an api with jwt verifiers\n\n
\nRoute plugins:\nJwt verification only\n
\n\nA Jwt verifier is the guard that verifies the signature of tokens in requests. \n\nA verifier can obvisouly verify or generate.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Your first jwt verifier\n\nLet's start by validating all incoming request tokens tokens on our simple route created in the @ref:[Before you start](#before-you-start) section.\n\n1. Navigate to the simple route\n2. Search in the list of plugins and add the `Jwt verification only` plugin on the flow\n3. Click on `Start by select or create a JWT Verifier`\n4. Create a new JWT verifier\n5. Set `simple-jwt-verifier` as `Name`\n6. Select `Hmac + SHA` as `Algo` (for this example, we expect tokens with a symetric signature), `512` as `SHA size` and `otoroshi` as `HMAC secret`\n7. Confirm the creation \n\nSave your route and try to call it\n\n```sh\ncurl -X GET 'http://myservice.oto.tools:8080/' --include\n```\n\nThis should output : \n```json\n{\n \"Otoroshi-Error\": \"error.expected.token.not.found\"\n}\n```\n\nA simple way to generate a token is to use @link:[jwt.io](http://jwt.io) { open=new }. Once navigate, define `HS512` as `alg` in header section and insert `otoroshi` as verify signature secret. \n\nOnce created, copy-paste the token from jwt.io to the Authorization header and call our service.\n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"X-JWT-Token: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `X-JWT-Token` in headers field. Its value is exactly the same as the passed token.\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"X-JWT-Token\": \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ipDFgkww51mSaSg_199BMRj4gK20LGz_czozu3u8rCFFO1X20MwcabSqEzUc0q4qQ4rjTxjoR4HeUDVcw8BxoQ\",\n ...\n }\n}\n```\n\n### Verify and generate a new token\n\nAn other feature is to verify the incomings tokens and generate new ones, with a different signature and claims. \n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify and re-sign JWT token`. \n2. Edit the name with `jwt-verify-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Save your verifier.\n\n> Note : the name of the verifier doesn't impact the identifier. So you can save the changes of your verifier without modifying the identifier used in your call. \n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"Authorization: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `authorization` in headers field. This time, the value are different and you can check his signature on @link:[jwt.io](https://jwt.io) { open=new } (the expected secret of the generated token is **otoroshi-internal-secret**)\n\n\n\n### Verify, transform and generate a new token\n\nThe most advanced verifier is able to do the same as the previous ones, with the ability to configure the token generation (claims, output header name).\n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify, transform and re-sign JWT token`. \n\n2. Edit the name with `jwt-verify-transform-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Set `Internal-Authorization` as `Header name`\n6. Set `key` on first field of `Rename token fields` and `from-otoroshi-verifier` on second field\n7. Set `generated-key` and `generated-value` as `Set token fields`\n8. Add `generated_at` and `${date}` as second field of `Set token fields` (Otoroshi supports an @ref:[expression language](../topics/expression-language.md))\n9. Save your verifier and try to call your service again.\n\nThis should output a json with `authorization` in headers field and our generate token in `Internal-Authorization`.\nOnce paste in @link:[jwt.io](https://jwt.io) { open=new }, you should have :\n\n\n\nYou can see, in the payload of your token, the two claims **from-otoroshi-verifier** and **generated-key** added during the generation of the token by the JWT verifier.\n"},{"name":"secure-app-with-auth0.md","id":"/how-to-s/secure-app-with-auth0.md","url":"/how-to-s/secure-app-with-auth0.html","title":"Secure an app with Auth0","content":"# Secure an app with Auth0\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Configure an Auth0 client\n\nThe first step of this tutorial is to setup an Auth0 application with the information of the instance of our Otoroshi.\n\nNavigate to @link:[https://manage.auth0.com](https://manage.auth0.com) { open=new } (create an account if it's not already done). \n\nLet's create an application when clicking on the **Applications** button on the sidebar. Then click on the **Create application** button on the top right.\n\n1. Choose `Regular Web Applications` as `Application type`\n2. Then set for example `otoroshi-client` as `Name`, and confirm the creation\n3. Jump to the `Settings` tab\n4. Scroll to the `Application URLs` section and add the following url as `Allowed Callback URLs` : `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n5. Set `https://otoroshi.oto.tools:8080/` as `Allowed Logout URLs`\n6. Set `https://otoroshi.oto.tools:8080` as `Allowed Web Origins` \n7. Save changes at the bottom of the page.\n\nOnce done, we have a full setup, with a client ID and secret at the top of the page, which authorizes our Otoroshi and redirects the user to the callback url when they log into Auth0.\n\n### Create an Auth0 provider module\n\nLet's back to Otoroshi to create an authentication module with `OAuth2 / OIDC provider` as `type`.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth provider` in the type selector field\n3. Then click on `Get from OIDC config` and paste `https://..auth0.com/.well-known/openid-configuration`. Replace the tenant name by the name of your tenant (displayed on the left top of auth0 page), and the region of the tenant (`eu` in my case).\n\nOnce done, set the `Client ID` and the `Client secret` from your Auth0 application. End the configuration with `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`.\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs).\n\n### Connect to Otoroshi with Auth0 authentication\n\nTo secure Otoroshi with your Auth0 configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n2. Scroll to the **BackOffice auth. settings**\n3. Select your last Authentication configuration (created in the previous section)\n4. Save the global configuration with the button on the top right\n\n#### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the *Login using third-party* button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the auth0 server login page\n4. Set your account credentials\n5. Good works! You're connected to Otoroshi with an Auth0 module.\n\n### Secure an app with Auth0 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new **Authentication module** configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a connected user by a third-party server is covered by another route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called **a private app** when it is protected by an Authentication module.\n\nWe can set the Authentication module on your route.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n5. Now you can try to call your route and see the Auth0 login page appears.\n\n\n"},{"name":"secure-app-with-keycloak.md","id":"/how-to-s/secure-app-with-keycloak.md","url":"/how-to-s/secure-app-with-keycloak.html","title":"Secure an app with Keycloak","content":"# Secure an app with Keycloak\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running a keycloak instance with docker\n\n```sh\ndocker run \\\n -p 8080:8080 \\\n -e KEYCLOAK_USER=admin \\\n -e KEYCLOAK_PASSWORD=admin \\\n --name keycloak-server \\\n --detach jboss/keycloak:15.0.1\n```\n\nThis should download the image of keycloak (if you haven't already it) and display the digest of the created container. This command mapped TCP port 8080 in the container to port 8080 of your laptop and created a server with `admin/admin` as admin credentials.\n\nOnce started, you can open a browser on @link:[http://localhost:8080](http://localhost:8080) { open=new } and click on `Administration Console`. Log to your instance with `admin/admin` as credentials.\n\nThe first step is to create a Keycloak client, an entity that can request Keycloak to authenticate a user. Click on the **clients** button on the sidebar, and then on **Create** button at the top right of the view.\n\nFill the client form with the following values.\n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Protocol`: `openid-connect`\n* `Root URL`: `http://otoroshi.oto.tools:8080/`\n\nValidate the creation of the client by clicking on the **Save** button.\n\nThe next step is to change the `Access Type` used by default. Jump to the `Access Type` field and select `confidential`. The confidential configuration force the client application to send at Keycloak a client ID and a client Secret. Scroll to the bottom of the page and save the configuration.\n\nNow scroll to the top of your page. Just at the right of the `Settings` tab, a new tab appeared : the `Credentials` page. Click on this tab, and make sure that `Client Id and Secret` is selected as `Client Authenticator` and copy the generated `Secret` to the next part.\n\n### Create a Keycloak provider module\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth2 / OIDC provider` in the type selector field\n3. Set a basic name and description\n\nA simple way to import a Keycloak client is to give the `URL of the OpenID Connect` Otoroshi. By default, keycloak used the next URL : `http://localhost:8080/auth/realms/master/.well-known/openid-configuration`. \n\nClick on the `Get from OIDC config` button and paste the previous link. Once it's done, scroll to the `URLs` section. All URLs has been fill with the values picked from the JSON object returns by the previous URL.\n\nThe only fields to change are : \n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Secret`: Paste the secret from the Credentials Keycloak page. In my case, it's something like `90c9bf0b-2c0c-4eb0-aa02-72195beb9da7`\n* `Callback URL`: `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs). Nothing else to change, just save the configuration.\n\n### Connect to Otoroshi with Keycloak authentication\n\nTo secure Otoroshi with your Keycloak configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new })\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the keycloak login page\n4. Set `admin/admin` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an Keycloak module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Visualize an admin user session or a private user session\n\nEach user, wheter connected user to the Otoroshi UI or at a private Otoroshi app, has an own session. As an administrator of Otoroshi, you can visualize via Otoroshi the list of the connected users and their profile.\n\nLet's start by navigating to the `Admin users sessions` page (just @link:[here](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/admin) or when clicking on the cog, and on the `Admins sessions` button at the bottom of the list).\n\nThis page gives a complete view of the connected admins. For each admin, you have his connection date and his expiration date. You can also check the `Profile` and the `Rights` of the connected users.\n\nIf we check the profile and the rights of the previously logged user (from Keycloak in the previous part) we can retrieve the following information :\n\n```json\n{\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"upn\": \"admin\",\n \"email_verified\": false,\n \"address\": {},\n \"groups\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ],\n \"preferred_username\": \"admin\"\n}\n```\n\nand his default rights \n\n```sh\n[\n {\n \"tenant\": \"default:rw\",\n \"teams\": [\n \"default:rw\"\n ]\n }\n]\n```\n\nWe haven't create any specific groups in Keycloak or specify rights in Otoroshi for him. In this case, the use received the default Otoroshi rights at his connection. The user can navigate on the default Organization and Teams (which are two resources created by Otoroshi at the boot) and have the full access on its (`r`: Read, `w`: Write, `*`: read/write).\n\nIn the same way, you'll find all users connected to a private Otoroshi app when navigate on the @link:[`Private App View`](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) or using the cog at the top of the page. \n\n### Configure the Keycloak module to force logged in users to be an Otoroshi admin with full access\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Supers admin only` button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, click on the cog button, and check that you have access to the full features of Otoroshi (like Admin user sessions). Now, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n]\n```\n\n### Merge Id token content on user profile\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Read profile` from token button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, your profile should be contains all Keycloak id token : \n```json\n{\n \"exp\": 1634286674,\n \"iat\": 1634286614,\n \"auth_time\": 1634286614,\n \"jti\": \"eb368578-e886-4caa-a51b-c1d04973c80e\",\n \"iss\": \"http://localhost:8080/auth/realms/master\",\n \"aud\": [\n \"master-realm\",\n \"account\"\n ],\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"typ\": \"Bearer\",\n \"azp\": \"keycloak-otoroshi-backoffice\",\n \"session_state\": \"e44fe471-aa3b-477d-b792-4f7b4caea220\",\n \"acr\": \"1\",\n \"allowed-origins\": [\n \"http://otoroshi.oto.tools:8080\"\n ],\n \"realm_access\": {\n \"roles\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ]\n },\n \"resource_access\": {\n \"master-realm\": {\n \"roles\": [\n \"view-identity-providers\",\n \"view-realm\",\n \"manage-identity-providers\",\n \"impersonation\",\n \"create-client\",\n \"manage-users\",\n \"query-realms\",\n \"view-authorization\",\n \"query-clients\",\n \"query-users\",\n \"manage-events\",\n \"manage-realm\",\n \"view-events\",\n \"view-users\",\n \"view-clients\",\n \"manage-authorization\",\n \"manage-clients\",\n \"query-groups\"\n ]\n },\n \"account\": {\n \"roles\": [\n \"manage-account\",\n \"manage-account-links\",\n \"view-profile\"\n ]\n }\n }\n ...\n}\n```\n\n### Manage the Otoroshi user rights from keycloak\n\nOne powerful feature supports by Otoroshi, is to use the Keycloak groups attributes to set a list of rights for a Otoroshi user.\n\nIn the Keycloak module, you have a field, named `Otoroshi rights field name` with `otoroshi_rights` as default value. This field is used by Otoroshi to retrieve information from the Id token groups.\n\nLet's create a group in Keycloak, and set our default Admin user inside.\nIn Keycloak admin console :\n\n1. Navigate to the groups view, using the keycloak sidebar\n2. Create a new group with `my-group` as `Name`\n3. Then, on the `Attributes` tab, create an attribute with `otoroshi_rights` as `Key` and the following json array as `Value`\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\nWith this configuration, the user have a full access on all Otoroshi resources (my-future-team is not created in Otoroshi but it's not a problem, Otoroshi can handle it and use this rights only when the team will be present)\n\nClick on the **Add** button and **save** the group. The last step is to assign our user to this group. Jump to `Users` view using the sidebar, click on **View all users**, edit the user and his group membership using the `Groups` tab (use **join** button the assign user in `my-group`).\n\nThe next step is to add a mapper in the Keycloak client. By default, Keycloak doesn't expose any users information (like group membership or users attribute). We need to ask to Keycloak to expose the user attribute `otoroshi_rights` set previously on group.\n\nNavigate to the `Keycloak-otoroshi-backoffice` client, and jump to `Mappers` tab. Create a new mapper with the following values: \n\n* Name: `otoroshi_rights`\n* Mapper Type: `User Attribute`\n* User Attribute: `otoroshi_rights`\n* Token Claim Name: `otoroshi_rights`\n* Claim JSON Type: `JSON`\n* Multivalued: `√`\n* Aggregate attribute values: `√`\n\nGo back to the Authentication Keycloak module inside Otoroshi UI, and turn off **Super admins only**. **Save** the configuration.\n\nOnce done, try again the connection to Otoroshi using Keycloak third-party server.\nNow, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\n### Secure an app with Keycloak authentication\n\nThe only change to apply on the previous authentication module is on the callback URL. When you want secure a Otoroshi service, and transform it on `Private App`, you need to set the `Callback URL` at `http://privateapps.oto.tools:8080/privateapps/generic/callback`. This configuration will redirect users to the backend service after they have successfully logged in.\n\n1. Go back to the authentication module\n2. Jump to the `Callback URL` field\n3. Paste this value `http://privateapps.oto.tools:8080/privateapps/generic/callback`\n4. Save your configuration\n5. Navigate to `http://myservice.oto.tools:8080`.\n6. You should redirect to the keycloak login page.\n7. Once logged in, you can check the content of the private app session created.\n\nThe rights should be : \n\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```"},{"name":"secure-app-with-ldap.md","id":"/how-to-s/secure-app-with-ldap.md","url":"/how-to-s/secure-app-with-ldap.html","title":"Secure an app and/or your Otoroshi UI with LDAP","content":"# Secure an app and/or your Otoroshi UI with LDAP\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n#### Running an simple OpenLDAP server \n\nRun OpenLDAP docker image : \n```sh\ndocker run \\\n -p 389:389 \\\n -p 636:636 \\\n --env LDAP_ORGANISATION=\"Otoroshi company\" \\\n --env LDAP_DOMAIN=\"otoroshi.tools\" \\\n --env LDAP_ADMIN_PASSWORD=\"otoroshi\" \\\n --env LDAP_READONLY_USER=\"false\" \\\n --env LDAP_TLS\"false\" \\\n --env LDAP_TLS_ENFORCE\"false\" \\\n --name my-openldap-container \\\n --detach osixia/openldap:1.5.0\n```\n\nLet's make the first search in our LDAP container :\n\n```sh\ndocker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=otoroshi,dc=tools -D \"cn=admin,dc=otoroshi,dc=tools\" -w otoroshi\n```\n\nThis should output :\n```sh\n# extended LDIF\n ...\n# otoroshi.tools\ndn: dc=otoroshi,dc=tools\nobjectClass: top\nobjectClass: dcObject\nobjectClass: organization\no: Otoroshi company\ndc: otoroshi\n\n# search result\nsearch: 2\nresult: 0 Success\n...\n```\n\nNow you can seed the open LDAP server with a few users. \n\nJoin your LDAP container.\n\n```sh\ndocker exec -it my-openldap-container \"/bin/bash\"\n```\n\nThe command `ldapadd` needs of a file to run.\n\nLaunch this command to create a `bootstrap.ldif` with one organization, one singers group with John user and a last group with Baz as scientist.\n\n```sh\necho -e \"\ndn: ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: People\n\ndn: ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: Role\n\ndn: uid=john,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: john\ncn: John\nsn: Brown\nmail: john@otoroshi.tools\npostalCode: 88442\nuserPassword: password\n\ndn: uid=baz,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: baz\ncn: Baz\nsn: Wilson\nmail: baz@otoroshi.tools\npostalCode: 88443\nuserPassword: password\n\ndn: cn=singers,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: singers\nmember: uid=john,ou=People,dc=otoroshi,dc=tools\n\ndn: cn=scientists,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: scientists\nmember: uid=baz,ou=People,dc=otoroshi,dc=tools\n\" > bootstrap.ldif\n\nldapadd -x -w otoroshi -D \"cn=admin,dc=otoroshi,dc=tools\" -f bootstrap.ldif -v\n```\n\n### Create an Authentication configuration\n\n- Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n- Click on the cog icon on the top right\n- Then `Authentication configs` button\n- And add a new configuration when clicking on the `Add item` button\n- Select the `Ldap auth. provider` in the type selector field\n- Set a basic name and description\n- Then set `ldap://localhost:389` as `LDAP Server URL`and `dc=otoroshi,dc=tools` as `Search Base`\n- Create a group filter (in the next part, we'll change this filter to spread users in different groups with given rights) with \n - objectClass=groupOfNames as `Group filter` \n - All as `Tenant`\n - All as `Team`\n - Read/Write as `Rights`\n- Set the search filter as `(uid=${username})`\n- Set `cn=admin,dc=otoroshi,dc=tools` as `Admin username`\n- Set `otoroshi` as `Admin password`\n- At the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n\n At this point, your configuration should be similar to :\n \n\n\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n- Test the connection when clicking on `Test admin connection` button. This should show a `It works!` message\n\n- Finally, test the user connection button and set `john/password` or `baz/password` as credentials. This should show a `It works!` message\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n\n### Connect to Otoroshi with LDAP authentication\n\nTo secure Otoroshi with your LDAP configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n- Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n- Scroll to the **BackOffice auth. settings**\n- Select your last Authentication configuration (created in the previous section)\n- Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n- Disconnect from your instance\n- Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080/backoffice/auth0/login](http://otoroshi.oto.tools:8080/backoffice/auth0/login) { open=new })\n- Set `john/password` or `baz/password` as credentials\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n\n#### Secure an app with LDAP authentication\n\nOnce the configuration is done, you can secure any of Otoroshi routes. \n\n- Navigate to any created route\n- Add the `Authentication` plugin to your route\n- Select your Authentication config inside the list\n- Save your configuration\n\nNow try to call your route. The login module should appear.\n\n#### Manage LDAP users rights on Otoroshi\n\nFor each group filter, you can affect a list of rights:\n\n- on an `Organization`\n- on a `Team`\n- and a level of rights : `Read`, `Write` or `Read/Write`\n\n\nStart by navigate to your authentication configuration (created in @ref:[previous](#create-an-authentication-configuration) step).\n\nThen, replace the values of the `Mapping group filter` field to match LDAP groups with Otoroshi rights.\n\n\n\n\nWith this configuration, Baz is an administrator of Otoroshi with full rights (read / write) on all organizations.\n\nConversely, John can't see any configuration pages (like the danger zone) because he has only the read rights on Otoroshi.\n\nYou can easily test this behaviour by @ref:[testing](#testing-your-configuration) with both credentials.\n\n\n#### Advanced usage of LDAP Authentication\n\nIn the previous section, we have define rights for each LDAP groups. But in some case, we want to have a finer granularity like set rights for a specific user. The last 4 fields of the authentication form cover this. \n\nLet's start by adding few properties for each connected users with `Extra metadata`.\n\n```json\n// Add this configuration in extra metadata part\n{\n \"provider\": \"OpenLDAP\"\n}\n```\n\nThe next field `Data override` is merged with extra metadata when a user connects to a `private app` or to the UI (inside Otoroshi, private app is a service secure by any authentication module). The `Email field name` is configured to match with the `mail` field from LDAP user data.\n\n```json \n{\n \"john@otoroshi.tools\": {\n \"stage_name\": \"Will\"\n }\n}\n```\n\nIf you try to connect to an app with this configuration, the user result profile should be :\n\n```json\n{\n ...,\n \"metadata\": {\n \"lastname\": \"Willy\",\n \"stage_name\": \"Will\"\n }\n}\n```\n\nLet's try to increase the John rights with the `Additional rights group`.\n\nThis field supports the creation of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n\n```json\n// increase_john_rights is a virtual group which adds full access rights at john \n{\n \"increase_john_rights\": {\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ],\n \"users\": [\n \"john@otoroshi.tools\"\n ]\n }\n}\n```\n\nThe last field `Rights override` is useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights. \n\nTo resume, when John connects to Otoroshi, he receives the rights to only read the default Organization (from **Mapping group filter**), then he is promote to administrator role (from **Additional rights group**) and finally his rights are reset with the last field **Rights override** to the read rights.\n\n```json \n{\n \"john@otoroshi.tools\": [\n {\n \"tenant\": \"*:r\",\n \"teams\": [\n \"*:r\"\n ]\n }\n ]\n}\n```\n\n\n\n\n\n\n\n\n"},{"name":"secure-the-communication-between-a-backend-app-and-otoroshi.md","id":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.md","url":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html","title":"Secure the communication between a backend app and Otoroshi","content":"# Secure the communication between a backend app and Otoroshi\n\n
\nRoute plugins:\nOtoroshi challenge token\n
\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nLet's create a new route with the Otorochi challenge plugin enabled.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 8081,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiChallenge\",\n \"config\": {\n \"version\": 2,\n \"ttl\": 30,\n \"request_header_name\": \"Otoroshi-State\",\n \"response_header_name\": \"Otoroshi-State-Resp\",\n \"algo_to_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"algo_from_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"state_resp_leeway\": 10\n }\n }\n ]\n}\nEOF\n```\n\nLet's use the following application, developed in NodeJS, which supports both versions of the exchange protocol.\n\nClone this @link:[repository](https://github.com/MAIF/otoroshi/blob/master/demos/challenge) and run the installation of the dependencies.\n\n```sh\ngit clone 'git@github.com:MAIF/otoroshi.git' --depth=1\ncd ./otoroshi/demos/challenge\nnpm install\nPORT=8081 node server.js\n```\n\nThe last command should return : \n\n```sh\nchallenge-verifier listening on http://0.0.0.0:8081\n```\n\nThis project runs an express client with one middleware. The middleware handles each request, and check if the header `State token header` is present in headers. By default, the incoming expected header is `Otoroshi-State` by the application and `Otoroshi-State-Resp` header in the headers of the return request. \n\nTry to call your service via http://myapi.oto.tools:8080/. This should return a successful response with all headers received by the backend app. \n\nNow try to disable the middleware in the nodejs file by commenting the following line. \n\n```js\n// app.use(OtoroshiMiddleware());\n```\n\nTry to call again your service. This time, Otoroshi breaks the return response from your backend service, and returns.\n\n```sh\nDownstream microservice does not seems to be secured. Cancelling request !\n```"},{"name":"secure-with-apikey.md","id":"/how-to-s/secure-with-apikey.md","url":"/how-to-s/secure-with-apikey.html","title":"Secure an api with api keys","content":"# Secure an api with api keys\n\n
\nRoute plugins:\nApikeys\n
\n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n### Create a simple route\n\n**From UI**\n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/routes](http://otoroshi.oto.tools:8080/bo/dashboard/routes) { open=new } and click on the `create new route` button\n2. Give a name to your route\n3. Save your route\n4. Set `myservice.oto.tools` as frontend domains\n5. Set `https://mirror.otoroshi.io` as backend target (hostname: `mirror.otoroshi.io`, port: `443`, Tls: `Enabled`)\n\n**From Admin API**\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n### Secure routes with api key\n\nBy default, a route is public. In our case, we want to secure all paths starting with `/api` and leave all others unauthenticated.\n\nLet's add a new plugin, called `Apikeys`, to our route. Search in the list of plugins, then add it to the flow.\nOnce done, restrict its range by setting up `/api` in the `Informations>include` section.\n\n**From Admin API**\n\n```sh\ncurl -X PUT http://otoroshi-api.oto.tools:8080/api/routes/myservice \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/api\"\n ],\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to @link:[http://myservice.oto.tools:8080/api/test](http://myservice.oto.tools:8080/api/test) { open=new } again. If the service is configured, you should have a `Service Not found error`.\n\nThe expected error on the `/api/test`, indicate that an api key is required to access to this part of the backend service.\n\nNavigate to any other routes which are not starting by `/api/*` like @link:[http://myservice.oto.tools:8080/test/bar](http://myservice.oto.tools:8080/test/bar) { open=new }\n\n\n### Generate an api key to request secure services\n\nNavigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add](http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add) { open=new } or when clicking on the **Add apikey** button on the sidebar.\n\nThe only required fields of an Otoroshi api key are : \n\n* `ApiKey id`\n* `ApiKey Secret`\n* `ApiKey Name`\n\nThese fields are automatically generated by Otoroshi. However, you can override these values and indicate an additional description.\n\nTo simplify the rest of the tutorial, set the values:\n\n* `my-first-api-key-id` as `ApiKey Id`\n* `my-first-api-key-secret` as `ApiKey Secret`\n\nClick on **Create and stay on this ApiKey** button at the bottom of the page.\n\nNow you created the key, it's time to call our previous generated service with it.\n\nOtoroshi supports 4 methods to achieve that: \n\nFirst one by passing Otoroshi api key in two headers : `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` (these headers names can be override on each service).\nThe second by passing Otoroshi api key in the authentication Header (basically the `Authorization` header) as a basic encoded value. The third option is to use the bearer generated for your apikey (you can get it by calling `curl http://otoroshi-api.oto.tools:8080/api/apikeys/my-first-api-key-id/bearer`). A fourth option is to use jwt token but we will not review it here but you can find a @ref[tutorial here](./secure-with-oauth2-client-credentials.md).\n\nLet's go ahead and call our service with the first method :\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the second method using basic authentication:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nor\n\n```sh\ncurl -X GET \\\n -u my-first-api-key-id:my-first-api-key-secret \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the third method using otoroshi bearer:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Bearer otoapk_my-first-api-key-id_99cb8e081d692044593ad0e658a67a5114f7afbdcbeb26f8087cce0df3b610b2' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\n> Tips : To easily fill your headers, you can jump to the `Call examples` section in each api key view. In this section the header names are the default values and the service url is not set. You have to adapt these lines to your case. \n\n### Override defaults headers names for a route\n\nIn some case, we want to change the defaults headers names (and it's a quite good idea).\n\nLet's start by navigating to the `Apikeys` plugin in the Designer of our route.\n\nThe first values to change are the headers names used to read the api key from client. Start by clicking on `extractors > CustomHeaders` and set the following values :\n\n* `api-key-header-id` as `Custom client id header name`\n* `api-key-header-secret` as `Custom client secret header name`\n\nSave the route, and call the service again.\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output an error because Otoroshi are expecting the api keys in other headers.\n\n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nCall one again the service but with the changed headers names.\n\n```sh\ncurl -X GET \\\n -H 'api-key-header-id: my-first-api-key-id' \\\n -H 'api-key-header-secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nAll others default services will continue to accept the api keys with the `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` headers, whereas our service, will accept the `api-key-header-id` and `api-key-header-secret` headers.\n\n### Accept only api keys with expected values\n\nBy default, a secure service only accepts requests with api key. But all generated api keys are eligible to call our service and in some case, we want authorize only a couple of api keys.\n\nYou can restrict the list of accepted api keys by giving a list of `metadata` or/and `tags`. Each api key has a list of `tags` and `metadata`, which can be used by Otoroshi to validate a request with an api key. All api key metadata/tags can be forward to your service (see `Otoroshi Challenge` section of a service to get more information about `Otoroshi info. token`).\n\nLet's starting by only accepting api keys with the `otoroshi` tag.\n\nClick on the `ApiKeys` plugin, and enabled the `Routing` section. These constraints guarantee that a request will only be transmitted if all the constraints are validated.\n\nIn our first case, set `otoroshi` in `One Tag in` array and save the service.\nThen call our service with :\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output :\n```json\n// Error reason : Our api key doesn't contains the expected tag.\n{\n \"Otoroshi-Error\": \"Bad API key\"\n}\n```\n\nNavigate to the edit page of our api key, and jump to the `Metadata and tags` section.\nIn this section, add `otoroshi` in `Tags` array, then save the api key. Call once again your call and you will normally get a successful response of our backend service.\n\nIn this example, we have limited our service to API keys that have `otoroshi` as a tag.\n\nOtoroshi provides a few others behaviours. For each behaviour, *Api key used should*:\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n\n---\n\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n \n----\n\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata"},{"name":"secure-with-oauth1-client.md","id":"/how-to-s/secure-with-oauth1-client.md","url":"/how-to-s/secure-with-oauth1-client.html","title":"Secure an app with OAuth1 client flow","content":"# Secure an app with OAuth1 client flow\n\n
\nRoute plugins:\nAuthentication\n
\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running an simple OAuth 1 server\n\nIn this tutorial, we'll instantiate a oauth 1 server with docker. If you alredy have the necessary, skip this section @ref:[to](#create-an-oauth-1-provider-module).\n\nLet's start by running the server\n\n```sh\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://otoroshi.oto.tools:8080/backoffice/auth0/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nWe created a oauth 1 server which accepts `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Redirect URI`. This URL is used by Otoroshi to retrieve a token and a profile at the end of an authentication process.\n\nAfter this command, the container logs should output :\n```sh \n127.0.0.1 - - [14/Oct/2021 12:10:49] \"HEAD /api/health HTTP/1.1\" 200 -\n```\n\n### Create an OAuth 1 provider module\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then **Authentication configs** button\n1. And add a new configuration when clicking on the **Add item** button\n2. Select the `Oauth1 provider` in the type selector field\n3. Set a basic name and description like `oauth1-provider`\n4. Set `2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET` as `Consumer key`\n5. Set `wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp` as `Consumer secret`\n6. Set `http://localhost:5000/oauth/request_token` as `Request Token URL`\n7. Set `http://localhost:5000/oauth/authorize` as `Authorize URL`\n8. Set `http://localhost:oauth/access_token` as `Access token URL`\n9. Set `http://localhost:5000/api/me` as `Profile URL`\n10. Set `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`\n11. At the bottom of the page, disable the **secure** button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n At this point, your configuration should be similar to :\n\n\n\n\nWith this configuration, the connected user will receive default access on teams and organizations. If you want to change the access rights for a specific user, you can achieve it with the `Rights override` field and a configuration like :\n\n```json\n{\n \"foo@example.com\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n}\n```\n\nSave your configuration at the bottom of the page, then navigate to the `danger zone` to use your module as a third-party connection to the Otoroshi UI.\n\n### Connect to Otoroshi with OAuth1 authentication\n\nTo secure Otoroshi with your OAuth1 configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the oauth 1 server login page\n4. Set `example-user` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an OAuth1 module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Secure an app with OAuth 1 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new auth module configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a logged user by a third-party server is cover by an other route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called a private app when it is protected by an authentication module.\n\nOur example server supports only one redirect URI. We need to kill it, and to create a new container with `http://otoroshi.oto.tools:8080/privateapps/generic/callback` as `OAUTH1_REDIRECT_URI`\n\n```sh\ndocker rm -f oauth1-server\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://privateapps.oto.tools:8080/privateapps/generic/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nOnce the authentication module and the new container created, we can define the authentication module on the service.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n\nNow you can try to call your route and see the login module appears.\n\n> \n\nThe allow access to the user.\n\n> \n\nIf you had any errors, make sure of :\n\n* check if you are on http or https, and if the **secure cookie option** is enabled or not on the authentication module\n* check if your OAuth1 server has the REDIRECT_URI set on **privateapps/...**\n* Make sure your server supports POST or GET OAuth1 flow set on authentication module\n\nOnce the configuration is working, you can check, when connecting with an Otoroshi admin user, the `Private App session` created (use the cog at the top right of the page, and select `Priv. app sesssions`, or navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) { open=new }).\n\nOne interesing feature is to check the profile of the connected user. In our case, when clicking on the `Profile` button of the right user, we should have : \n\n```json\n{\n \"email\": \"foo@example.com\",\n \"id\": 1,\n \"name\": \"test name\",\n \"screen_name\": \"example-user\"\n}\n```"},{"name":"secure-with-oauth2-client-credentials.md","id":"/how-to-s/secure-with-oauth2-client-credentials.md","url":"/how-to-s/secure-with-oauth2-client-credentials.html","title":"Secure an app with OAuth2 client_credential flow","content":"# Secure an app with OAuth2 client_credential flow\n\n\n\nOtoroshi makes it easy for your app to implement the [OAuth2 Client Credentials Flow](https://auth0.com/docs/authorization/flows/client-credentials-flow). \n\nWith machine-to-machine (M2M) applications, the system authenticates and authorizes the app rather than a user. With the client credential flow, applications will pass along their Client ID and Client Secret to authenticate themselves and get a token.\n\n## Deployed the Client Credential Service\n\nThe Client Credential Service must be enabled as a global plugin on your Otoroshi instance. Once enabled, it will expose three endpoints to issue and validate tokens for your routes.\n\nLet's navigate to your otoroshi instance (in our case http://otoroshi.oto.tools:8080) on the danger zone (`top right cog icon / Danger zone` or at [/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone)).\n\nTo enable a plugin in global on Otoroshi, you must add it in the `Global Plugins` section.\n\n1. Open the `Global Plugin` section \n2. Click on `enabled` (if not already done)\n3. Search the plugin named `Client Credential Service` of type `Sink` (you need to enabled it on the old or new Otoroshi engine, depending on your use case)\n4. Inject the default configuration by clicking on the button (if you are using the old Otoroshi engine)\n\nIf you click on the arrow near each plugin, you will have the documentation of the plugin and its default configuration.\n\nThe client credential plugin has by default 4 parameters : \n\n* `domain`: a regex used to expose the three endpoints (`default`: *)\n* `expiration`: duration until the token expire (in ms) (`default`: 3600000)\n* `defaultKeyPair`: a key pair used to sign the jwt token. By default, Otoroshi is deployed with an otoroshi-jwt-signing that you can visualize on the jwt verifiers certificates (`default`: \"otoroshi-jwt-signing\")\n* `secure`: if enabled, Otoroshi will expose routes only in the https requests case (`default`: true)\n\nIn this tutorial, we will set the configuration as following : \n\n* `domain`: oauth.oto.tools\n* `expiration`: 3600000\n* `defaultKeyPair`: otoroshi-jwt-signing\n* `secure`: false\n\nNow that the plugin is running, third routes are exposed on each matching domain of the regex.\n\n* `GET /.well-known/otoroshi/oauth/jwks.json` : retrieve all public keys presents in Otoroshi\n* `POST /.well-known/otoroshi/oauth/token/introspect` : validate and decode the token \n* `POST /.well-known/otoroshi/oauth/token` : generate a token with the fields provided\n\nOnce the global configuration saved, we can deployed a simple service to test it.\n\nLet's navigate to the routes page, and create a new route with : \n\n1. `foo.oto.tools` as `domain` in the frontend node\n2. `mirror.otoroshi.io` as hostname in the list of targets of the backend node, and `443` as `port`.\n3. Search in the list of plugins and add the `Apikeys` plugin to the flow\n4. In the extractors section of the `Apikeys` plugin, disabled the `Basic`, `Client id` and `Custom headers` option.\n5. Save your route\n\nLet's make a first call, to check if the jwks are already exposed :\n\n```sh\ncurl 'http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json'\n```\n\nThe output should look like a list of public keys : \n```sh\n{\n \"keys\": [\n {\n \"kty\": \"RSA\",\n \"e\": \"AQAB\",\n \"kid\": \"otoroshi-intermediate-ca\",\n ...\n }\n ...\n ]\n}\n``` \n\nLet's make a call to your route. \n\n```sh\ncurl 'http://foo.oto.tools:8080/'\n```\n\nThis should output the expected error: \n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nThe first step is to generate an api key. Navigate to the api keys page, and create an item with the following values (it will be more easy to use them in the next step)\n\n* `my-id` as `ApiKey Id`\n* `my-secret` as `ApiKey Secret`\n\nThe next step is to get a token by calling the endpoint `http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json`. The required fields are the grand type, the client and the client secret corresponding to our generated api key.\n\n```sh\ncurl -X POST http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/token \\\n-H \"Content-Type: application/json\" \\\n-d @- <<'EOF'\n{\n \"grant_type\": \"client_credentials\",\n \"client_id\":\"my-id\",\n \"client_secret\":\"my-secret\"\n}\nEOF\n```\n\nThis request have one more optional field, named `scope`. The scope can be used to set a bunch of scope on the generated access token.\n\nThe last command should look like : \n\n```sh\n{\n \"access_token\": \"generated-token-xxxxx\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600\n}\n```\n\nNow we can call our api with the generated token\n\n```sh\ncurl 'http://foo.oto.tools:8080/' \\\n -H \"Authorization: Bearer generated-token-xxxxx\"\n```\n\nThis should output a successful call with the list of headers with a field named `Authorization` containing the previous access token.\n\n## Other possible configuration\n\nBy default, Otoroshi generate the access token with the specified key pair in the configuration. But, in some case, you want a specific key pair by client_id/client_secret.\nThe `jwt-sign-keypair` metadata can be set on any api key with the id of the key pair as value. \n"},{"name":"setup-otoroshi-cluster.md","id":"/how-to-s/setup-otoroshi-cluster.md","url":"/how-to-s/setup-otoroshi-cluster.html","title":"Setup an Otoroshi cluster","content":"# Setup an Otoroshi cluster\n\n
\nRoute plugins:\nAdditional Headers In\n
\n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n"},{"name":"tailscale-integration.md","id":"/how-to-s/tailscale-integration.md","url":"/how-to-s/tailscale-integration.html","title":"Tailscale integration","content":"# Tailscale integration\n\n[Tailscale](https://tailscale.com/) is a VPN service that let you create your own private network based on [Wireguard](https://www.wireguard.com/). Tailscale goes beyond the simple meshed wireguard based VPN and offers out of the box NAT traversal, third party identity provider integration, access control, magic DNS and let's encrypt integration for the machines on your VPN.\n\nOtoroshi provides somes plugins out of the box to work in a [Tailscale](https://tailscale.com/) environment.\n\nby default Otoroshi, works out of the box when integrated in a `tailnet` as you can contact other machines usign their ip address. But we can go a little bit further.\n\n## tailnet configuration\n\nfirst thing, go to your tailnet setting on [tailscale.com](https://login.tailscale.com/admin/machines) and go to the [DNS tab](https://login.tailscale.com/admin/dns). Here you can find \n\n* your tailnet name: the domain name of all your machines on your tailnet\n* MagicDNS: a way to address your machines by directly using their names\n* HTTPS Certificates: HTTPS certificates provision for all your machines\n\nto use otoroshi Tailscale plugin you must enable `MagicDNS` and `HTTPS Certificates`\n\n## Tailscale certificates integration\n\nyou can use tailscale generated let's encrypt certificates in otoroshi by using the `Tailscale certificate fetcher job` in the plugins section of the danger zone. Once enabled, this job will fetch certificates for domains in `xxxx.ts.net` that belong to your tailnet. \n\nas usual, the fetched certificates will be available in the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) of otoroshi.\n\n## Tailscale targets integration\n\nthe following pair of plugins let your contact tailscale machine by using their names even if their are multiple instance.\n\nwhen you register a machine on a tailnet, you have to provide a name for it, let say `my-server`. This machine will be addressable in your tailnet with `my-server.tailxxx.ts.net`. But if you have multiple instance of the same server on several machines with the same `my-server` name, their DNS name on the tailnet will be `my-server.tailxxx.ts.net`, `my-server-1.tailxxx.ts.net`, `my-server-2.tailxxx.ts.net`, etc. If you want to use those names in an otoroshi backend it could be tricky if the application has something like autoscaling enabled.\n\nin that case, you can add the `Tailscale targets job` in the plugins section of the danger zone. Once enabled, this job will fetch periodically available machine on the tailnet with their names and DNS names. Then, in a route, you can use the `Tailscale select target by name` plugin to tell otoroshi to loadbalance traffic between all machine that have the name specified in the plugin config. instead of their DNS name."},{"name":"tls-termination-using-own-certificates.md","id":"/how-to-s/tls-termination-using-own-certificates.md","url":"/how-to-s/tls-termination-using-own-certificates.html","title":"TLS termination using your own certificates","content":"# TLS termination using your own certificates\n\nThe goal of this tutorial is to expose a service via https using a certificate generated by openssl.\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nTry to call the service.\n\n```sh\ncurl 'http://myservice.oto.tools:8080'\n```\n\nThis should output something like\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463145856319359618\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8080\",\n \"opun-gateway-request-id\": \"1463145856554240100\",\n \"x-forwarded-proto\": \"https\",\n },\n \"body\": \"\"\n}\n```\n\nLet's try to call the service in https.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nThis should output\n\n```sh\ncurl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to myservice.oto.tools:8443\n```\n\nTo fix it, we have to generate a certificate and import it in Otoroshi to match the domain `myservice.oto.tools`.\n\n> If you already had a certificate you can skip the next set of commands and directly import your certificate in Otoroshi\n\nWe will use openssl to generate a private key and a self-signed certificate.\n\n```sh\nopenssl genrsa -out myservice.key 4096\n# remove pass phrase\nopenssl rsa -in myservice.key -out myservice.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key myservice.key -out myservice.cer -subj \"/CN=myservice.oto.tools\"\n```\n\nCheck the content of the certificate \n\n```sh\nopenssl x509 -in myservice.cer -text\n```\n\nThis should contains something like\n\n```sh\nCertificate:\n Data:\n Version: 1 (0x0)\n Serial Number: 9572962808320067790 (0x84d9fef455f188ce)\n Signature Algorithm: sha256WithRSAEncryption\n Issuer: CN=myservice.oto.tools\n Validity\n Not Before: Nov 23 14:25:55 2021 GMT\n Not After : Nov 23 14:25:55 2022 GMT\n Subject: CN=myservice.oto.tools\n Subject Public Key Info:\n Public Key Algorithm: rsaEncryption\n Public-Key: (4096 bit)\n Modulus:\n...\n```\n\nOnce generated, go back to Otoroshi and navigate to the certificates management page (`top right cog icon / SSL/TLS certificates` or at @link:[`/bo/dashboard/certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates)) and click on `Add item`.\n\nSet `myservice-certificate` as `name` and `description`.\n\nDrop the `myservice.cer` file or copy the content to the `Certificate full chain` field.\n\nDo the same action for the `myservice.key` file in the `Certificate private key` field.\n\nSet your passphrase password in the `private key password` field if you added one.\n\nLet's try the same call to the service.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nAn error should occurs due to the untrsuted received certificate server\n\n```sh\ncurl: (60) SSL certificate problem: self signed certificate\nMore details here: https://curl.haxx.se/docs/sslcerts.html\n\ncurl failed to verify the legitimacy of the server and therefore could not\nestablish a secure connection to it. To learn more about this situation and\nhow to fix it, please visit the web page mentioned above.\n```\n\nEnd this tutorial by trusting the certificate server \n\n```sh\ncurl 'https://myservice.oto.tools:8443' --cacert myservice.cer\n```\n\nThis should finally output\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463158439730479893\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8443\",\n \"opun-gateway-request-id\": \"1463158439558515871\",\n \"x-forwarded-proto\": \"https\",\n \"sozu-id\": \"01FN6MGKSYZNJYHEMP4R5PJ4Q5\"\n },\n \"body\": \"\"\n}\n```\n\n"},{"name":"tls-using-lets-encrypt.md","id":"/how-to-s/tls-using-lets-encrypt.md","url":"/how-to-s/tls-using-lets-encrypt.html","title":"TLS termination using Let's Encrypt","content":"# TLS termination using Let's Encrypt\n\nAs you know, Otoroshi is capable of doing TLS termination for your services. You can import your own certificates, generate certificates from scratch and you can also use the @link:[ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555) to generate certificates. One of the most popular service offering ACME certificates creation is @link:[Let's Encrypt](https://letsencrypt.org/).\n\n@@@ warning\nIn order to make this tutorial work, your otoroshi instance MUST be accessible from the internet in order to be reachable by Let's Encrypt ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n@@@ note\nthis tutorial can work with any ACME provider with the same rules. your otoroshi instance MUST be accessible by the ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n## Setup let's encrypt on otoroshi\n\nGo on the danger zone page by clicking on the [`cog icon / Danger Zone`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Scroll to the `Let's Encrypt settings` section. Enable it, and specify the address of the ACME server (for production Let's Encrypt it's `acme://letsencrypt.org`, for testing, it's `acme://letsencrypt.org/staging`. Any ACME server address should work). You can also add one or more email addresses or contact urls that will be included in your Let's Encrypt account. You don't have to fill the `public/private key` inputs as they will be automatically generated on the first usage.\n\n## Creating let's encrypt certificate from FQDNs\n\nYou can go to the certificates page by clicking on the [`cog icon / SSL/TLS Certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Here, click on the `+ Let's Encrypt certificate` button. A popup will show up to ask you the FQDN that you want for you certificate. Once done, click on the `Create` button. A few moment later, you will be redirected on a brand new certificate generated by Let's encrypt. You can now enjoy accessing your service behind the FQDN with TLS.\n\n## Creating let's encrypt certificate from a service\n\nYou can go to any service page and enable the flag `Issue Let's Encrypt cert.`. Do not forget to save your service. A few moment later, the certificates will be available in the certificates page and you can will be able to enjoy accessing your service with TLS.\n"},{"name":"wasm-usage.md","id":"/how-to-s/wasm-usage.md","url":"/how-to-s/wasm-usage.html","title":"Using wasm plugins","content":"# Using wasm plugins\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. You can find more about those plugins @ref:[here](../topics/wasm-usage.md)\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create the route with the plugin validator](#create-the-route-with-the-plugin-validator)\n3. [Test your validator](#test-your-validator)\n4. [Update the route by replacing the backend with a WASM file](#update-the-route-by-replacing-the-backend-with-a-wasm-file)\n5. [WASM backend test](#wasm-backend-test)\n6. [Expose a single file as WASM backend](#expose-a-single-file-as-wasm-backend)\n\nAfter completing these steps you will have a route that uses WASM plugins written in Rust.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create the route with the plugin validator\n\nFor this tutorial, we will start with an existing wasm file. The main function of this file will check the value of an http header to allow access or not. The can find this file at [https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm)\n\nThe main function of this validator, written in rust, should look like:\n\nvalidator.rs\n: @@snip [validator.rs](../snippets/wasmo/validator.rs) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.ts\n: @@snip [validator.ts](../snippets/wasmo/validator.ts) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.go\n: @@snip [validator.js](../snippets/wasmo/validator.go) \n\nThe plugin receives the request context from Otoroshi (the matching route, the api key if present, the headers, etc) as `WasmAccessValidatorContext` object. \nThen it applies a check on the headers, and responds with an error or success depending on the content of the foo header. \nObviously, the previous snippet is an example and the editor allows you to write whatever you want as a check.\n\nLet's create a route that uses the previous wasm file as an access validator plugin :\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThis request will apply the following process:\n\n* names the route *demo-otoroshi*\n* creates a frontend exposed on the `demo-otoroshi.oto.tools` \n* forward requests on one target, reachable at `mirror.otoroshi.io` using TLS on port 443\n* adds the *WasmAccessValidator* plugin to validate access based on the foo header to the route\n\nYou can validate the route creation by navigating to the [dashboard](http://otoroshi.oto.tools:8080/bo/dashboard/routes/demo-otoroshi?tab=flow)\n\n## Test your validator\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -I\n```\n\nThis should output the following error:\n\n```\nHTTP/1.1 401 Unauthorized\n```\n\nLet's call again the route by adding the header foo with the bar value.\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -I\n```\n\nThis should output the successfull message:\n\n```\nHTTP/1.1 200 OK\n```\n\n## Update the route by replacing the backend with a WASM file\n\nThe next step in this tutorial is to use a WASM file as backend of the route. We will use an existing WASM file, available in our wasm demos repository on github. \nThe content of this plugin, called `wasm-target.wasm`, looks like:\n\ntarget.rs\n: @@snip [target.rs](../snippets/wasmo/target.rs) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.ts\n: @@snip [target.ts](../snippets/wasmo/target.ts) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.go\n: @@snip [target.js](../snippets/wasmo/target.go) \n\nLet's explain this snippet. The purpose of this type of plugin is to respond an HTTP response with http status, body and headers map.\n\n1. Includes all public structures from `types.rs` file. This file contains predefined Otoroshi structures that plugins can manipulate.\n2. Necessary imports. [Extism](https://extism.org/docs/overview)'s goal is to make all software programmable by providing a plug-in system. \n3. Creates a map of new headers that will be merged with incoming request headers.\n4. Creates the response object with the map of merged headers, a simple JSON body and a successfull status code.\n\nThe file is downloadable [here](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm).\n\nLet's update the route using the this wasm file.\n\n```sh\ncurl -X PUT \"http://otoroshi-api.oto.tools:8080/api/routes/demo-otoroshi\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmBackend\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThe response should contains the updated route content.\n\n## WASM backend test\n\nLet's call our route.\n\n```sh\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -H \"fifi: foo\" -v\n```\n\nThis should output:\n\n```\n* Trying 127.0.0.1:8080...\n* Connected to demo-otoroshi.oto.tools (127.0.0.1) port 8080 (#0)\n> GET / HTTP/1.1\n> Host: demo-otoroshi.oto.tools:8080\n> User-Agent: curl/7.79.1\n> Accept: */*\n> foo:bar\n> fifi:foo\n>\n* Mark bundle as not supporting multiuse\n< HTTP/1.1 200 OK\n< foo: bar\n< Host: demo-otoroshi.oto.tools:8080\n<\n* Closing connection 0\n{\"foo\": \"bar\"}\n```\n\nIn this response, we can find our headers send in the curl command and those added by the wasm plugin.\n\n\n## Expose a single file as WASM backend\n\nA WASM backend plugin can directly expose a file written in Wasmo. This is the simplest possibility to write HTML, Javascript, JSON or expose a simple PNG image.\n\nLet's expose a HTML page. In your Wasmo instance, execute the following instructions:\n\n1. Click the new plugin button\n2. Add a name and `validate`\n3. Click the new plugin\n4. Create a new file named `index.html`\n5. Copy and paste the following content\n\n```html\n \n\n\n Wasmo plugin\n\n\n

Hello from Wasmo

\n\n\n```\n\nThis snippet is a short HTML template with a title to indicate that it comes from Wasmo. \n\nNow we can write our javascript function to parse and return the content of our HTML to Otoroshi. \n\n1. Navigate to the `index.js` file\n2. Replace the content with the following content\n\n```js\nimport IndexPage from './index.html'\n\nexport function execute() {\n \n let response = {\n headers: {\n 'Content-Type': 'text/html; charset=utf-8'\n },\n body: IndexPage,\n status: 200\n };\n \n Host.outputString(JSON.stringify(response));\n\n return 0;\n}\n```\n\nThe code is pretty self-explanatory. We start by importing our HTML page and we build the response with the correct content type, the body and a 200 http status.\n\nJust before testing, we need to change the esbuild configuration to specify how to bundle the HTML file.\n\nThe contents of your `esbuild.js` file should look this:\n\n```js\nconst esbuild = require('esbuild');\n\nesbuild\n .build({\n entryPoints: ['index.js'],\n outdir: 'dist',\n bundle: true,\n loader: {\n '.html': 'text'\n },\n sourcemap: true,\n minify: false, // might want to use true for production build\n format: 'cjs', // needs to be CJS for now\n target: ['es2020'] // don't go over es2020 because quickjs doesn't support it\n })\n```\n\nCheck your browser at `http://demo-otoroshi.oto.tools:8080` and you should see your page content updated to the new text.\n\nIf you need to expose more than a HTML page, we highly recommend to use the @ref:[Zip Backend plugin](../how-to-s/zip-backend-plugin.md)"},{"name":"wasmo-installation.md","id":"/how-to-s/wasmo-installation.md","url":"/how-to-s/wasmo-installation.html","title":"Deploy your own Wasmo","content":"# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: http://localhost:5001\n* `Apikey id`: admin-api-apikey-id\n* `Apikey secret`: admin-api-apikey-secret\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n"},{"name":"working-with-eureka.md","id":"/how-to-s/working-with-eureka.md","url":"/how-to-s/working-with-eureka.html","title":"Working with Eureka","content":"# Working with Eureka\n\n\n\nEureka is a library of Spring Cloud Netflix, which provides two parts to register and discover services.\nGenerally, the services are applications written with Spring but Eureka also provides a way to communicate in REST. The main goals of Eureka are to allow clients to find and communicate with each other without hard-coding the hostname and port.\nAll services are registered in an Eureka Server.\n\nTo work with Eureka, Otoroshi has three differents plugins:\n\n* to expose its own Eureka Server instance\n* to discover an existing Eureka Server instance\n* to use Eureka application as an Otoroshi target and took advantage of all Otoroshi clients features (load-balancing, rate limiting, etc...)\n\nLet's cut this tutorial in three parts. \n\n- Create an simple Spring application that we'll use as an Eureka Client\n- Deploy an implementation of the Otoroshi Eureka Server (using the `Eureka Instance` plugin), register eureka clients and expose them using the `Internal Eureka Server` plugin\n- Deploy an Netflix Eureka Server and use it in Otoroshi to discover apps using the `External Eureka Server` plugin.\n\n\nIn this tutorial: \n\n- [Create an Otoroshi route with the Internal Eureka Server plugin](#create-an-otoroshi-route-with-the-internal-eureka-server-plugin)\n- [Create a simple Eureka Client and register it](#create-a-simple-eureka-client-and-register-it)\n- [Connect to an external Eureka server](#connect-to-an-external-eureka-server)\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create an Otoroshi route with the Internal Eureka Server plugin\n\n@@@ note\nWe'll supposed that you have an Otoroshi exposed on the 8080 port with the new Otoroshi engine enabled\n@@@\n\nLet's jump to the routes Otoroshi [view](http://otoroshi.oto.tools:8080/bo/dashboard/routes) and create a new route using the wizard button.\n\nEnter the following values in for each step:\n\n1. An Eureka Server instance\n2. Choose the first choice : **BLANK ROUTE** and click on continue\n3. As exposed domain, set `eureka-server.oto.tools/eureka`\n4. As Target URL, set `http://foo.bar` (this value has no importance and will be skip by the Otoroshi Instance plugin)\n5. Validate the creation\n\nOnce created, you can hide with the arrow on the right top of the screen the tester view (which is displayed by default after each route creation).\nIn our case, we want to add a new plugin, called Internal Eureka Instance on our feed.\n\nInside the designer view:\n\n1. Search the `Eureka Instance` in the list of plugins.\n2. Add it to the feed by clicking on it\n3. Set an eviction timeout at 300 seconds (this configuration is used by Otoroshi to automatically check if an Eureka is up. Otherwise Otoroshi will evict the eureka client from the registry)\n\nWell done you have set up an Eureka Server. To check the content of an Eureka Server, you can navigate to this [link]('http://otoroshi.oto.tools:8080/bo/dashboard/eureka-servers'). In all case, none instances or applications are registered, so the registry is currently empty.\n\n### Create a simple Eureka Client and register it\n\n*This tutorial has no vocation to teach you how to write an Spring application and it may exists a newer version of this Spring code.*\n\n\nFor this tutorial, we'll use the following code which initiates an Eureka Client and defines an Spring REST Controller with only one endpoint. This endpoint will return its own exposed port (this value will be useful to check that the Otoroshi load balancing is right working between the multiples Eureka instances registered).\n\n\nLet's fast create a Spring project using [Spring Initializer](https://start.spring.io/). You can use the previous link or directly click on the following link to get the form already filled with the needed dependencies.\n\n````bash\nhttps://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.3&packaging=jar&jvmVersion=17&groupId=otoroshi.io&artifactId=eureka-client&name=eureka-client&description=A%20simple%20eureka%20client&packageName=otoroshi.io.eureka-client&dependencies=cloud-eureka,web\n````\n\nFeel free to change the project metadata for your use case.\n\nOnce downloaded and uncompressed, let's ahead and start to delete the application.properties and create an application.yml (if you are more comfortable with an application.properties, keep it)\n\n````yaml\neureka:\n client:\n fetch-registry: false # disable the discovery services mechanism for the client\n serviceUrl:\n defaultZone: http://eureka-server.oto.tools:8080/eureka\n\nspring:\n application:\n name: foo_app\n\n````\n\n\nNow, let's define the simple REST controller to expose the client port.\n\nCreate a new file, called PortController.java, in the sources folder of your project with the following content.\n\n````java\npackage otoroshi.io.eurekaclient;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.env.Environment;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class PortController {\n\n @Autowired\n Environment environment;\n\n @GetMapping(\"/port\")\n public String index() {\n return environment.getProperty(\"local.server.port\");\n }\n}\n````\nThis controller is very simple, we just exposed one endpoint `/port` which returns the port as string. Our client is ready to running. \n\nLet's launch it with the following command:\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085\n````\n\n@@@note\nThe port is not required but it will be useful when we will deploy more than one instances in the rest of the tutorial\n@@@\n\n\nOnce the command ran, you can navigate to the eureka server view in the Otoroshi UI. The dashboard should displays one registered app and instance.\nIt should also displays a timer for each application which represents the elapsed time since the last received heartbeat.\n\nLet's define a new route to exposed our registered eureka client.\n\n* Create a new route, named `Eureka client`, exposed on `http://eureka-client.oto.tools:8080` and targeting `http://foo.bar`\n* Search and add the `Internal Eureka server` plugin \n* Edit the plugin and choose your eureka server and your app (in our case, `Eureka Server` and `FOO_APP` respectively)\n* Save your route\n\nNow try to call the new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n````\n\nIf everything is working, you should get the port 8085 as the response.The setup is working as expected, but we can improve him by scaling our eureka client.\n\nOpen a new tab in your terminal and run the following command.\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8083\n````\n\nJust wait a few seconds and retry to call your new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\n````\n\nThe configuration is ready and the setup is working, Otoroshi use all instances of your app to dispatch clients on it.\n\n### Connect to an external Eureka server\n\nOtoroshi has the possibility to discover services by connecting to an Eureka Server.\n\nLet's create a route with an Eureka application as Otoroshi target:\n\n* Create a new blank API route\n* Search and add the `External Eureka Server` plugin\n* Set your eureka URL\n* Click on `Fetch Services` button to discover the applications of the Eureka instance\n* In the appeared selector, choose the application to target\n* Once the frontend configured, save your route and try to call it.\n\nWell done, you have exposed your Eureka application through the Otoroshi discovery services.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"name":"zip-backend-plugin.md","id":"/how-to-s/zip-backend-plugin.md","url":"/how-to-s/zip-backend-plugin.html","title":"Quickly expose a website and static files ","content":"# Quickly expose a website and static files \n\n@@include[badge.md](../includes/badge.md) { #badge }\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create an archive with HTML and CSS files](#create-an-archive-with-html-and-css-files)\n2. [Use the Zip Backend Plugin](#use-the-zip-backend-plugin)\n\nAfter completing these steps, you will be able to statically expose any kind of files from an archive.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create an archive with HTML and CSS files\n\nLet's start by creating an archive composed of html and css files.\n\nThe contents of your `index.html` file should be likes this:\n\n```html\n\n\n\n Wasmo plugin\n \n\n\n

Hello from Wasmo

\n\n\n```\n\nThe contents of your `index.css` file should be likes this:\n\n```css\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nOnce created, you can create the archive of both.\n\n```sh\nzip bundle.zip index.html index.css\n```\n\n## Use the Zip Backend Plugin \n\nLet's create the route using the Otoroshi admin API. The route content is pretty simple, a few fields about the name and the frontend, and the Zip Backend plugin in the plugins list.\n\nDon't forget to change the default `path-to-the-zip-file` with your path.\n\n``` sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"demootoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"debug\": false,\n \"plugin\": \"cp:otoroshi.next.plugins.ZipFileBackend\",\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"url\": \"file:///bundle.zip\",\n \"headers\": {},\n \"dir\": \"./zips\",\n \"prefix\": null,\n \"ttl\": 3600000\n }\n }\n ]\n}\nEOF\n```\n\nCalling the route in a new browser tab at `http://demo-otoroshi.oto.tools:8080/`. You should see something like the following image:\n\n@@@ div { .centered-img }\n\n@@@\n\nAs we can see, the content of the archive is available, our HTML page is served and the CSS, linked into the HTML page, has loaded.\n\nYou can check this behaviour by calling the following path: \n\n```bash\ncurl http://demo-otoroshi.oto.tools:8080/index.css -v\n```\n\nThe result should be like:\n\n```bash\n< HTTP/1.1 200 OK\n< Transfer-Encoding: chunked\n< Content-Type: text/css\n<\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nCongratulations - You have just exposed your first archive. Do not hesitate to expose any type of content."},{"name":"badge.md","id":"/includes/badge.md","url":"/includes/badge.html","title":"","content":"\n
\nRoute plugins:\n
Zip file backend plugin
\n
\n\n"},{"name":"experimental.md","id":"/includes/experimental.md","url":"/includes/experimental.html","title":"@@@ warning","content":"@@@ warning\n\nthis feature is **EXPERIMENTAL** and might not work as expected.
\nIf you encounter any bugs, [please fill an issue](https://github.com/MAIF/otoroshi/issues/new), it will help us a lot :)\n\n@@@\n"},{"name":"fetch-and-start.md","id":"/includes/fetch-and-start.md","url":"/includes/fetch-and-start.html","title":"","content":"\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n"},{"name":"initialize.md","id":"/includes/initialize.md","url":"/includes/initialize.html","title":"","content":"\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
\nSet up an Otoroshi\n\n
\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n"},{"name":"index.md","id":"/index.md","url":"/index.html","title":"Otoroshi","content":"# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.15.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n"},{"name":"get-otoroshi.md","id":"/install/get-otoroshi.md","url":"/install/get-otoroshi.html","title":"Get Otoroshi","content":"# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi-16.15.0-dev.zip\nunzip ./otoroshi-16.15.0-dev.zip\ncd otoroshi-16.15.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.15.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n"},{"name":"index.md","id":"/install/index.md","url":"/install/index.html","title":"Install","content":"# Install\n\nIn this sections, you will find informations about how to install and run Otoroshi\n\n* @ref:[Get Otoroshi](./get-otoroshi.md)\n* @ref:[Setup Otoroshi](./setup-otoroshi.md)\n* @ref:[Run Otoroshi](./run-otoroshi.md)\n\n@@@ index\n\n* [Get Otoroshi](./get-otoroshi.md)\n* [Setup Otoroshi](./setup-otoroshi.md)\n* [Run Otoroshi](./run-otoroshi.md)\n\n@@@\n"},{"name":"run-otoroshi.md","id":"/install/run-otoroshi.md","url":"/install/run-otoroshi.html","title":"Run Otoroshi","content":"# Run Otoroshi\n\nNow you are ready to run Otoroshi. You can run the following command with some tweaks depending on the way you want to configure Otoroshi. If you want to pass a custom configuration file, use the `-Dconfig.file=/path/to/file.conf` flag in the following commands.\n\n## From .zip file\n\n```sh\ncd otoroshi-vx.x.x\n./bin/otoroshi\n```\n\n## From .jar file\n\nFor Java 11\n\n```sh\njava -jar otoroshi.jar\n```\n\nif you want to run the jar file for on a JDK above JDK11, you'll have to add the following flags\n\n```sh\njava \\\n --add-opens=java.base/javax.net.ssl=ALL-UNNAMED \\\n --add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED \\\n --add-exports=java.base/sun.security.x509=ALL-UNNAMED \\\n --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \\\n -Dlog4j2.formatMsgNoLookups=true \\\n -jar otoroshi.jar\n```\n\n## From docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi\n```\n\nYou can also pass useful args like :\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi -Dconfig.file=/usr/app/otoroshi/conf/otoroshi.conf -Dlogger.file=/usr/app/otoroshi/conf/otoroshi.xml\n```\n\nIf you want to provide your own config file, you can read @ref:[the documentation about config files](./setup-otoroshi.md).\n\nYou can also provide some ENV variable using the `--env` flag to customize your Otoroshi instance.\n\nThe list of possible env variables is available @ref:[here](./setup-otoroshi.md).\n\nYou can use a volume to provide configuration like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/conf\" maif/otoroshi\n```\n\nYou can also use a volume if you choose to use `filedb` datastore like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd)/filedb:/usr/app/otoroshi/filedb\" maif/otoroshi -Dotoroshi.storage=file\n```\n\nYou can also use a volume if you choose to use exports files :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/imports\" maif/otoroshi -Dotoroshi.importFrom=/usr/app/otoroshi/imports/export.json\n```\n\n## Run examples\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -Dotoroshi.importFrom=/home/user/otoroshi.json \\\n -Dconfig.file=/home/user/otoroshi.conf \\\n -jar ./otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - Importing from: /home/user/otoroshi.json\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n\nIf you choose to start Otoroshi without importing existing data, Otoroshi will create a new admin user and print the login details in the log. When you will log into the admin dashboard, Otoroshi will ask you to create another account to avoid security issues.\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -jar otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / HHUsiF2UC3OPdmg0lGngEv3RrbIwWV5W\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n"},{"name":"setup-otoroshi.md","id":"/install/setup-otoroshi.md","url":"/install/setup-otoroshi.html","title":"Setup Otoroshi","content":"# Setup Otoroshi\n\nin this section we are going to configure otoroshi before running it for the first time\n\n## Setup the database\n\nRight now, Otoroshi supports multiple datastore. You can choose one datastore over another depending on your use case.\n\n@@@div { .plugin .platform } \n
Redis
\n\n
Recommended
\n\nThe **redis** datastore is quite nice when you want to easily deploy several Otoroshi instances.\n\n\n\n@link:[Documentation](https://redis.io/topics/quickstart)\n@@@\n\n@@@div { .plugin .platform } \n
In memory
\n\nThe **in-memory** datastore is kind of interesting. It can be used for testing purposes, but it is also a good candidate for production because of its fastness.\n\n\n\n@ref:[Start with](../getting-started.md)\n@@@\n\n@@@div { .plugin .platform } \n
Cassandra
\n\n
Clustering
\n\nExperimental support, should be used in cluster mode for leaders\n\n\n\n@link:[Documentation](https://cassandra.apache.org/doc/latest/cassandra/getting_started/installing.html)\n@@@\n\n@@@div { .plugin .platform } \n
Postgresql
\n\n
Clustering
\n\nOr any postgresql compatible databse like cockroachdb for instance (experimental support, should be used in cluster mode for leaders)\n\n\n\n@link:[Documentation](https://www.postgresql.org/docs/10/tutorial-install.html)\n@@@\n\n@@@div { .plugin .platform } \n\n
FileDB
\n\nThe **filedb** datastore is pretty handy for testing purposes, but is not supposed to be used in production mode. \nNot suitable for production usage.\n\n\n\n@@@\n\n\n@@@ div { .centered-img }\n\n@@@\n\nthe first thing to setup is what kind of datastore you want to use with the `otoroshi.storage` setting\n\n```conf\notoroshi {\n storage = \"inmemory\" # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n}\n```\n\ndepending on the value you chose, you will be able to configure your datastore with the following configuration\n\ninmemory\n: @@snip [inmemory.conf](../snippets/datastores/inmemory.conf) \n\nfile\n: @@snip [file.conf](../snippets/datastores/file.conf) \n\nhttp\n: @@snip [http.conf](../snippets/datastores/http.conf) \n\ns3\n: @@snip [s3.conf](../snippets/datastores/s3.conf) \n\nredis\n: @@snip [lettuce.conf](../snippets/datastores/lettuce.conf) \n\npostgresql\n: @@snip [pg.conf](../snippets/datastores/pg.conf) \n\ncassandra\n: @@snip [inmemory.conf](../snippets/datastores/cassandra.conf) \n\n## Setup your hosts before running\n\nBy default, Otoroshi starts with domain `oto.tools` that automatically targets `127.0.0.1` with no changes to your `/etc/hosts` file. Of course you can change the domain value, you have to add the values in your `/etc/hosts` file according to the setting you put in Otoroshi configuration or define the right ip address at the DNS provider level\n\n* `otoroshi.domain` => `mydomain.org`\n* `otoroshi.backoffice.subdomain` => `otoroshi`\n* `otoroshi.privateapps.subdomain` => `privateapps`\n* `otoroshi.adminapi.exposedSubdomain` => `otoroshi-api`\n* `otoroshi.adminapi.targetSubdomain` => `otoroshi-admin-internal-api`\n\nfor instance if you want to change the default domain and use something like `otoroshi.mydomain.org`, then start otoroshi like \n\n```sh\njava -Dotoroshi.domain=mydomain.org -jar otoroshi.jar\n```\n\n@@@ warning\nOtoroshi cannot be accessed using `http://127.0.0.1:8080` or `http://localhost:8080` because Otoroshi uses Otoroshi to serve it's own UI and API. When otoroshi starts with an empty database, it will create a service descriptor for that using `otoroshi.domain` and the settings listed on this page and in the here that serve Otoroshi API and UI on `http://otoroshi-api.${otoroshi.domain}` and `http://otoroshi.${otoroshi.domain}`.\nOnce the descriptor is saved in database, if you want to change `otoroshi.domain`, you'll have to edit the descriptor in the database or restart Otoroshi with an empty database.\n@@@\n\n@@@ warning\nif your otoroshi instance runs behind a reverse proxy (L4 / L7) or inside a docker container where exposed ports (that you will use to access otoroshi) are not the same that the ones configured in otoroshi (`http.port` and `https.port`), you'll have to configure otoroshi exposed port to avoid bad redirection URLs when using authentication modules and other otoroshi tools. To do that, just set the values of the exposed ports in `otoroshi.exposed-ports.http = $theExposedHttpPort` (OTOROSHI_EXPOSED_PORTS_HTTP) and `otoroshi.exposed-ports.https = $theExposedHttpsPort` (OTOROSHI_EXPOSED_PORTS_HTTPS)\n@@@\n\n## Setup your configuration file\n\nThere is a lot of things you can configure in Otoroshi. By default, Otoroshi provides a configuration that should be enough for testing purpose. But you'll likely need to update this configuration when you'll need to move into production.\n\nIn this page, any configuration property can be set at runtime using a `-D` flag when launching Otoroshi like \n\n```sh\njava -Dhttp.port=8080 -jar otoroshi.jar\n```\n\nor\n\n```sh\n./bin/otoroshi -Dhttp.port=8080 \n```\n\nif you want to define your own config file and use it on an otoroshi instance, use the following flag\n\n```sh\njava -Dconfig.file=/path/to/otoroshi.conf -jar otoroshi.jar\n``` \n\n### Example of a custom. configuration file\n\n```conf\ninclude \"application.conf\"\n\nhttp.port = 8080\n\napp {\n storage = \"inmemory\"\n importFrom = \"./my-state.json\"\n env = \"prod\"\n domain = \"oto.tools\"\n rootScheme = \"http\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n privateapps {\n subdomain = \"privateapps\"\n session {\n exp = 86400000\n }\n }\n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-api-apikey-id\"\n backOfficeApiKeyClientSecret = \"admin-api-apikey-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n }\n claim {\n sharedKey = \"mysecret\"\n }\n filedb {\n path = \"./filedb/state.ndjson\"\n }\n}\n\nplay.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2592000000\n domain = \".oto.tools\"\n cookieName = \"oto-sess\"\n }\n}\n```\n\n### Reference configuration\n\n@@snip [reference.conf](../snippets/reference.conf) \n\n### More config. options\n\nSee default configuration at\n\n* @link:[Base configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/base.conf) { open=new }\n* @link:[Application configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/application.conf) { open=new }\n\n## Configuration with env. variables\n\nEevery property in the configuration file can be overriden by an environment variable if it has env variable override written like `${?ENV_VARIABLE}`).\n\n## Reference configuration for env. variables\n\n@@snip [reference-env.conf](../snippets/reference-env.conf) \n"},{"name":"built-in-legacy-plugins.md","id":"/plugins/built-in-legacy-plugins.md","url":"/plugins/built-in-legacy-plugins.html","title":"Built-in legacy plugins","content":"# Built-in legacy plugins\n\nOtoroshi provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLog }\n\n## Access log (CLF)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in CLF format.\n\nLog format is the following:\n\n`\"$service\" $clientAddress - \"$userId\" [$timestamp] \"$host $method $path $protocol\" \"$status $statusTxt\" $size $snowflake \"$to\" \"$referer\" \"$userAgent\" $http $duration $errorMsg`\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLogJson }\n\n## Access log (JSON)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in json format.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.KafkaAccessLog }\n\n## Kafka access log\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `KafkaAccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged as an event in a kafka topic.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"KafkaAccessLog\": {\n \"enabled\": true,\n \"topic\": \"otoroshi-access-log\",\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KafkaAccessLog\" : {\n \"enabled\" : true,\n \"topic\" : \"otoroshi-access-log\",\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.BasicAuthCaller }\n\n## Basic Auth. caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\nThis plugin accepts the following configuration\n\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.OAuth2Caller }\n\n## OAuth2 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\nThis plugin accepts the following configuration\n\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.cache.ResponseCache }\n\n## Response Cache\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ResponseCache\": {\n \"enabled\": true, // enabled cache\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"autoClean\": true, // cleanup older keys when all bigger than maxSize\n \"filter\": { // cache only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ResponseCache\" : {\n \"enabled\" : true,\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.clientcert.ClientCertChainHeader }\n\n## Client certificate header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ClientCertChain`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ClientCertChain\": {\n \"pem\": { // send client cert as PEM format in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-Pem\"\n },\n \"dns\": { // send JSON array of DNs in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-DNs\"\n },\n \"chain\": { // send JSON representation of client cert chain in a header\n \"send\": true,\n \"header\": \"X-Client-Cert-Chain\"\n },\n \"claims\": { // pass JSON representation of client cert chain in the otoroshi JWT token\n \"send\": false,\n \"name\": \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCertChain\" : {\n \"pem\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-Pem\"\n },\n \"dns\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-DNs\"\n },\n \"chain\" : {\n \"send\" : true,\n \"header\" : \"X-Client-Cert-Chain\"\n },\n \"claims\" : {\n \"send\" : false,\n \"name\" : \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.defer.DeferPlugin }\n\n## Defer Responses\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"DeferPlugin\": {\n \"defaultDefer\": 0 // default defer in millis\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"DeferPlugin\" : {\n \"defaultDefer\" : 0\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.discovery.DiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoHeader }\n\n## Geolocation header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `GeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfoHeader\": {\n \"headerName\": \"X-Geolocation-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfoHeader\" : {\n \"headerName\" : \"X-Geolocation-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.hmac.HMACCallerPlugin }\n\n## HMAC caller plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HMACCallerPlugin`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACCallerPlugin\" : {\n \"secret\" : \"my-defaut-secret\",\n \"algo\" : \"HMAC-SHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiCanary }\n\n## Izanami Canary Campaign\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiCanary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiProxy }\n\n## Izanami APIs Proxy\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiProxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jq.JqBodyTransformer }\n\n## JQ bodies transformer\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `JqBodyTransformer`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\nSome JSON variables are accessible by default :\n\n * `$url`: the request url\n * `$path`: the request path\n * `$domain`: the request domain\n * `$method`: the request method\n * `$headers`: the current request headers (with name in lowercase)\n * `$queryParams`: the current request query params\n * `$otoToken`: the otoroshi protocol token (if one)\n * `$inToken`: the first matched JWT token as is (from verifiers, if one)\n * `$token`: the first matched JWT token as is (from verifiers, if one)\n * `$user`: the current user (if one)\n * `$apikey`: the current apikey (if one)\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jsoup.HtmlPatcher }\n\n## Html Patcher\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"HtmlPatcher\" : {\n \"appendHead\" : [ ],\n \"appendBody\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.log4j.Log4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `Log4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them.\n\nThis plugin can accept the following configuration\n\n```javascript\n{\n \"Log4ShellFilter\": {\n \"status\": 200, // the status send back when an attack expression is found\n \"body\": \"\", // the body send back when an attack expression is found\n \"parseBody\": false // enables request body parsing to find attack expression\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"Log4ShellFilter\" : {\n \"status\" : 200,\n \"body\" : \"\",\n \"parseBody\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.loggers.BodyLogger }\n\n## Body logger\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BodyLogger`\n\n### Description\n\nThis plugin can log body present in request and response. It can just logs it, store in in the redis store with a ttl and send it to analytics.\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"BodyLogger\": {\n \"enabled\": true, // enabled logging\n \"log\": true, // just log it\n \"store\": false, // store bodies in datastore\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"sendToAnalytics\": false, // send bodies to analytics\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"password\": \"password\", // password for the ui, if none, it's public\n \"filter\": { // log only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"BodyLogger\" : {\n \"enabled\" : true,\n \"log\" : true,\n \"store\" : false,\n \"ttl\" : 300000,\n \"sendToAnalytics\" : false,\n \"maxSize\" : 5242880,\n \"password\" : \"password\",\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.mirror.MirroringPlugin }\n\n## Mirroring plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `MirroringPlugin`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"MirroringPlugin\": {\n \"enabled\": true, // enabled mirroring\n \"to\": \"https://foo.bar.dev\", // the url of the service to mirror\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"MirroringPlugin\" : {\n \"enabled\" : true,\n \"to\" : \"https://foo.bar.dev\",\n \"captureResponse\" : false,\n \"generateEvents\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oauth1.OAuth1CallerPlugin }\n\n## OAuth1 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"OAuth1Caller\" : {\n \"algo\" : \"HmacSHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oidc.OIDCHeaders }\n\n## OIDC headers\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCHeaders\" : {\n \"profile\" : {\n \"send\" : true,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idtoken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accesstoken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.security.SecurityTxt }\n\n## Security Txt\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `SecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"SecurityTxt\": {\n \"Contact\": \"contact@foo.bar\", // mandatory, a link or e-mail address for people to contact you about security issues\n \"Encryption\": \"http://url-to-public-key\", // optional, a link to a key which security researchers should use to securely talk to you\n \"Acknowledgments\": \"http://url\", // optional, a link to a web page where you say thank you to security researchers who have helped you\n \"Preferred-Languages\": \"en, fr, es\", // optional\n \"Policy\": \"http://url\", // optional, a link to a policy detailing what security researchers should do when searching for or reporting security issues\n \"Hiring\": \"http://url\", // optional, a link to any security-related job openings in your organisation\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"SecurityTxt\" : {\n \"Contact\" : \"contact@foo.bar\",\n \"Encryption\" : \"https://...\",\n \"Acknowledgments\" : \"https://...\",\n \"Preferred-Languages\" : \"en, fr\",\n \"Policy\" : \"https://...\",\n \"Hiring\" : \"https://...\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.static.StaticResponse }\n\n## Static Response\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `StaticResponse`\n\n### Description\n\nThis plugin returns a static response for any request\n\n\n\n### Default configuration\n\n```json\n{\n \"StaticResponse\" : {\n \"status\" : 200,\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n },\n \"body\" : \"{\\\"message\\\":\\\"hello world!\\\"}\",\n \"bodyBase64\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/user-agent`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoHeader }\n\n## User-Agent header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `UserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfoHeader\": {\n \"headerName\": \"X-User-Agent-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfoHeader\" : {\n \"headerName\" : \"X-User-Agent-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.workflow.WorkflowEndpoint }\n\n## [DEPRECATED] Workflow endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `WorkflowEndpoint`\n\n### Description\n\nThis plugin runs a workflow and return the response\n\n\n\n### Default configuration\n\n```json\n{\n \"WorkflowEndpoint\" : {\n \"workflow\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.biscuit.BiscuitValidator }\n\n## Biscuit token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nThis plugin validates a Biscuit token.\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nexpected response from http service is\n\n```json\n{\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n}\n```\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"url\": \"...\", // url for the call\n \"headers\": {}, // http header for the call\n \"ttl\": 600000, // cache ttl,\n \"mtlsConfig\": {\n \"certId\": \"xxxxx\",\n \"mtls\": false,\n \"loose\": false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingHttpValidator\" : {\n \"url\" : \"http://foo.bar\",\n \"ttl\" : 600000,\n \"headers\" : { },\n \"mtlsConfig\" : {\n \"certId\" : \"...\",\n \"mtls\" : false,\n \"loose\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingValidator }\n\n## Client certificate matching\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\" : {\n \"serialNumbers\" : [ ],\n \"subjectDNs\" : [ ],\n \"issuerDNs\" : [ ],\n \"regexSubjectDNs\" : [ ],\n \"regexIssuerDNs\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertValidator }\n\n## Client Certificate Only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.hmac.HMACValidator }\n\n## HMAC access validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HMACAccessValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACAccessValidator\" : {\n \"secret\" : \"\"\n }\n}\n```\n\n\n\n### Documentation\n\n\n The HMAC signature needs to be set on the `Authorization` or `Proxy-Authorization` header.\n The format of this header should be : `hmac algorithm=\"\", headers=\"
\", signature=\"\"`\n As example, a simple nodeJS call with the expected header\n ```js\n const crypto = require('crypto');\n const fetch = require('node-fetch');\n\n const date = new Date()\n const secret = \"my-secret\" // equal to the api key secret by default\n\n const algo = \"sha512\"\n const signature = crypto.createHmac(algo, secret)\n .update(date.getTime().toString())\n .digest('base64');\n\n fetch('http://myservice.oto.tools:9999/api/test', {\n headers: {\n \"Otoroshi-Client-Id\": \"my-id\",\n \"Otoroshi-Client-Secret\": \"my-secret\",\n \"Date\": date.getTime().toString(),\n \"Authorization\": `hmac algorithm=\"hmac-${algo}\", headers=\"Date\", signature=\"${signature}\"`,\n \"Accept\": \"application/json\"\n }\n })\n .then(r => r.json())\n .then(console.log)\n ```\n In this example, we have an Otoroshi service deployed on http://myservice.oto.tools:9999/api/test, protected by api keys.\n The secret used is the secret of the api key (by default, but you can change it and define a secret on the plugin configuration).\n We send the base64 encoded date of the day, signed by the secret, in the Authorization header. We specify the headers signed and the type of algorithm used.\n You can sign more than one header but you have to list them in the headers fields (each one separate by a space, example : headers=\"Date KeyId\").\n The algorithm used can be HMAC-SHA1, HMAC-SHA256, HMAC-SHA384 or HMAC-SHA512.\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.oidc.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.quotas.ServiceQuotas }\n\n## Public quotas\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `ServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current service\n\n\n\n\n\n\n\n### Default configuration\n\n```json\n{\n \"ServiceQuotas\" : {\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.users.HasAllowedUsersValidator }\n\n## Allowed users only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasAllowedUsersValidator\": {\n \"usernames\": [], // allowed usernames\n \"emails\": [], // allowed user email addresses\n \"emailDomains\": [], // allowed user email domains\n \"metadataMatch\": [], // json path expressions to match against user metadata. passes if one match\n \"metadataNotMatch\": [], // json path expressions to match against user metadata. passes if none match\n \"profileMatch\": [], // json path expressions to match against user profile. passes if one match\n \"profileNotMatch\": [], // json path expressions to match against user profile. passes if none match\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasAllowedUsersValidator\" : {\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"emailDomains\" : [ ],\n \"metadataMatch\" : [ ],\n \"metadataNotMatch\" : [ ],\n \"profileMatch\" : [ ],\n \"profileNotMatch\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ApikeyAuthModule }\n\n## Apikey auth module\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"ApikeyAuthModule\" : {\n \"realm\" : \"apikey-auth-module-realm\",\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : [ ],\n \"oneMetaIn\" : [ ],\n \"allMetaIn\" : [ ],\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.CertificateAsApikey }\n\n## Client certificate as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `CertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"CertificateAsApikey\" : {\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ClientCredentialFlowExtractor }\n\n## Client Credential Flow ApiKey extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin can extract an apikey from an opaque access_token generate by the `ClientCredentialFlow` plugin\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.biscuit.BiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.discovery.DiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.IpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"apikey\": \"xxxxxxx\",\n \"timeout\": 2000, // timeout in ms\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"apikey\" : \"xxxxxxx\",\n \"timeout\" : 2000,\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.MaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"path\": \"/foo/bar/cities.mmdb\", // file path, can be \"global\"\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"path\" : \"global\",\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.jwt.JwtUserExtractor }\n\n## Jwt user extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `JwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"JwtUserExtractor\" : {\n \"verifier\" : \"\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\" : \"email\",\n \"metaPath\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.oidc.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenAsApikey\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.useragent.UserAgentExtractor }\n\n## User-Agent details extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `UserAgentInfo`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfo\": {\n \"log\": false // will log user-agent details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfo\" : {\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.apikeys.ClientCredentialService }\n\n## Client Credential Service\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `ClientCredentialService`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.discovery.DiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator }\n\n## Kubernetes admission validator webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to handle manifests validation\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector }\n\n## Kubernetes sidecar injector webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to inject otoroshi-sidecar in pods\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.jobs.StateExporter }\n\n## Otoroshi state exporter job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `StateExporter`\n\n### Description\n\nThis job send an event containing the full otoroshi export every n seconds\n\n\n\n### Default configuration\n\n```json\n{\n \"StateExporter\" : {\n \"every_sec\" : 3600,\n \"format\" : \"json\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleCertificatesFetcherJob }\n\n## Tailscale certificate fetcher job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will fetch certificates from Tailscale ACME provider\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleTargetsJob }\n\n## Tailscale targets job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will aggregates Tailscale possible online targets\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesIngressControllerJob }\n\n## Kubernetes Ingress Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi as an Ingress Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesOtoroshiCRDsControllerJob }\n\n## Kubernetes Otoroshi CRDs Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi CRDs Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesToOtoroshiCertSyncJob }\n\n## Kubernetes to Otoroshi certs. synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. TLS secrets from Kubernetes to Otoroshi\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.OtoroshiToKubernetesCertSyncJob }\n\n## Otoroshi certs. to Kubernetes secrets synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. Otoroshi certs to Kubernetes TLS secrets\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.wasm.WasmVmPoolCleaner }\n\n## otoroshi.wasm.WasmVmPoolCleaner\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.next.proxy.ProxyEngine }\n\n## Otoroshi next proxy engine (experimental)\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `NextGenProxyEngine`\n\n### Description\n\nThis plugin holds the next generation otoroshi proxy engine implementation. This engine is **experimental** and may not work as expected !\n\nYou can active this plugin only on some domain names so you can easily A/B test the new engine.\nThe new proxy engine is designed to be more reactive and more efficient generally.\nIt is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nThe idea is to only rely on plugins to work and avoid losing time with features that are not used in service descriptors.\nAn automated conversion happens for every service descriptor. If the exposed domain is handled by this plugin, it will be served by this plugin.\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `route-compositions`: multiple routing rules based on hostname, path, method and headers that will execute the same list of plugins\n - `backends`: a list of targets to contact a backend\n\nas an example, let say you want to use the new engine on your service exposed on `api.foo.bar/api`.\nTo do that, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration,\nenabled it and in `domains` add the value `api.foo.bar` (it is possible to use `*.foo.bar` if that's what you want to do).\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the old one.\n\n\n\n### Default configuration\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"domains\" : [ \"*\" ],\n \"deny_domains\" : [ ],\n \"reporting\" : true,\n \"merge_sync_steps\" : true,\n \"export_reporting\" : false,\n \"apply_legacy_checks\" : true,\n \"debug\" : false,\n \"capture\" : false,\n \"captureMaxEntitySize\" : 4194304,\n \"debug_headers\" : false,\n \"routing_strategy\" : \"tree\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.script.ForwardTrafficHandler }\n\n## Forward traffic\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `ForwardTrafficHandler`\n\n### Description\n\nThis plugin can be use to perform a raw traffic forward to an URL without passing through otoroshi routing\n\n\n\n### Default configuration\n\n```json\n{\n \"ForwardTrafficHandler\" : {\n \"domains\" : {\n \"my.domain.tld\" : {\n \"baseUrl\" : \"https://my.otherdomain.tld\",\n \"secret\" : \"jwt signing secret\",\n \"service\" : {\n \"id\" : \"service id for analytics\",\n \"name\" : \"service name for analytics\"\n }\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n\n\n"},{"name":"built-in-plugins.md","id":"/plugins/built-in-plugins.md","url":"/plugins/built-in-plugins.html","title":"Built-in plugins","content":"# Built-in plugins\n\nOtoroshi next provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration.\n\n
\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersIn }\n\n## Additional headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersIn`\n\n### Description\n\nThis plugin adds headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersOut }\n\n## Additional headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersOut`\n\n### Description\n\nThis plugin adds headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AllowHttpMethods }\n\n## Allowed HTTP methods\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AllowHttpMethods`\n\n### Description\n\nThis plugin verifies the current request only uses allowed http methods\n\n\n\n### Default configuration\n\n```json\n{\n \"allowed\" : [ ],\n \"forbidden\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyAuthModule }\n\n## Apikey auth module\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"realm\" : \"apikey-auth-module-realm\",\n \"matcher\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyCalls }\n\n## Apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyCalls`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass\n\n\n\n### Default configuration\n\n```json\n{\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyQuotas }\n\n## Apikey quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyQuotas`\n\n### Description\n\nIncrements quotas for the currents apikey. Useful when 'legacy checks' are disabled on a service/globally or when apikey are extracted in a custom fashion.\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AuthModule }\n\n## Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AuthModule`\n\n### Description\n\nThis plugin applies an authentication module\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BasicAuthCaller }\n\n## Basic Auth. caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : null,\n \"passaword\" : null,\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BrotliResponseCompressor }\n\n## Brotli compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BrotliResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using brotli\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BuildMode }\n\n## Build mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BuildMode`\n\n### Description\n\nThis plugin displays a build page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.CanaryMode }\n\n## Canary mode\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.CanaryMode`\n\n### Description\n\nThis plugin can split a portion of the traffic to canary backends\n\n\n\n### Default configuration\n\n```json\n{\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ContextValidation }\n\n## Context validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ContextValidation`\n\n### Description\n\nThis plugin validates the current context using JSONPath validators.\n\nThis plugin let you configure a list of validators that will check if the current call can pass.\nA validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value.\nThe JSONPath will be applied on a document that will look like\n\n```js\n{\n \"snowflake\" : \"1516772930422308903\",\n \"apikey\" : { // current apikey\n \"clientId\" : \"vrmElDerycXrofar\",\n \"clientName\" : \"default-apikey\",\n \"metadata\" : {\n \"foo\" : \"bar\"\n },\n \"tags\" : [ ]\n },\n \"user\" : null, // current user\n \"request\" : {\n \"id\" : 1,\n \"method\" : \"GET\",\n \"headers\" : {\n \"Host\" : \"ctx-validation-next-gen.oto.tools:9999\",\n \"Accept\" : \"*/*\",\n \"User-Agent\" : \"curl/7.64.1\",\n \"Authorization\" : \"Basic dnJtRWxEZXJ5Y1hyb2ZhcjpvdDdOSTkyVGI2Q2J4bWVMYU9UNzJxamdCU2JlRHNLbkxtY1FBcXBjVjZTejh0Z3I1b2RUOHAzYjB5SEVNRzhZ\",\n \"Remote-Address\" : \"127.0.0.1:58929\",\n \"Timeout-Access\" : \"\",\n \"Raw-Request-URI\" : \"/foo\",\n \"Tls-Session-Info\" : \"Session(1650461821330|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\" : [ ],\n \"tls\" : false,\n \"uri\" : \"/foo\",\n \"path\" : \"/foo\",\n \"version\" : \"HTTP/1.1\",\n \"has_body\" : false,\n \"remote\" : \"127.0.0.1\",\n \"client_cert_chain\" : null\n },\n \"config\" : {\n \"validators\" : [ {\n \"path\" : \"$.apikey.metadata.foo\",\n \"value\" : \"bar\"\n } ]\n },\n \"global_config\" : { ... }, // global config\n \"attrs\" : {\n \"otoroshi.core.SnowFlake\" : \"1516772930422308903\",\n \"otoroshi.core.ElCtx\" : {\n \"requestId\" : \"1516772930422308903\",\n \"requestSnowflake\" : \"1516772930422308903\",\n \"requestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\"\n },\n \"otoroshi.next.core.Report\" : \"otoroshi.next.proxy.NgExecutionReport@277b44e2\",\n \"otoroshi.core.RequestStart\" : 1650461821545,\n \"otoroshi.core.RequestWebsocket\" : false,\n \"otoroshi.core.RequestCounterOut\" : 0,\n \"otoroshi.core.RemainingQuotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 0,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 2,\n \"remainingCallsPerDay\" : 9999998,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 269,\n \"remainingCallsPerMonth\" : 9999731\n },\n \"otoroshi.next.core.MatchedRoutes\" : \"MutableList(route_022825450-e97d-42ed-8e22-b23342c1c7c8)\",\n \"otoroshi.core.RequestNumber\" : 1,\n \"otoroshi.next.core.Route\" : { ... }, // current route as json\n \"otoroshi.core.RequestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\",\n \"otoroshi.core.ApiKey\" : { ... }, // current apikey as json\n \"otoroshi.core.User\" : { ... }, // current user as json\n \"otoroshi.core.RequestCounterIn\" : 0\n },\n \"route\" : { ... },\n \"token\" : null // current valid jwt token if one\n}\n```\n\nthe expected value support some syntax tricks like\n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current apikey has a metadata name `foo` with a value containing `bar`, you can write the following validator\n\n```js\n{\n \"path\": \"$.apikey.metadata.foo\",\n \"value\": \"Contains(bar)\"\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"validators\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Cors }\n\n## CORS\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Cors`\n\n### Description\n\nThis plugin applies CORS rules\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_origin\" : \"*\",\n \"expose_headers\" : [ ],\n \"allow_headers\" : [ ],\n \"allow_methods\" : [ ],\n \"excluded_patterns\" : [ ],\n \"max_age\" : null,\n \"allow_credentials\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.DisableHttp10 }\n\n## Disable HTTP/1.0\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.DisableHttp10`\n\n### Description\n\nThis plugin forbids HTTP/1.0 requests\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EndlessHttpResponse }\n\n## Endless HTTP responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EndlessHttpResponse`\n\n### Description\n\nThis plugin returns 128 Gb of 0 to the ip addresses is in the list\n\n\n\n### Default configuration\n\n```json\n{\n \"finger\" : false,\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaServerSink }\n\n## Eureka instance\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaServerSink`\n\n### Description\n\nEureka plugin description\n\n\n\n### Default configuration\n\n```json\n{\n \"evictionTimeout\" : 300\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaTarget }\n\n## Internal Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an internal Eureka server.\n If you want to use a target which it locate outside of Otoroshi, you must use the External Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ExternalEurekaTarget }\n\n## External Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ExternalEurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an external Eureka server.\n If you want to use a target that is directly exposed by an implementation of Eureka by Otoroshi,\n you must use the Internal Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForceHttpsTraffic }\n\n## Force HTTPS traffic\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForceHttpsTraffic`\n\n### Description\n\nThis plugin verifies the current request uses HTTPS\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForwardedHeader }\n\n## Forwarded header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForwardedHeader`\n\n### Description\n\nThis plugin adds all the Forwarded header to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalMaintenanceMode }\n\n## Global Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalMaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page for every services. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalPerIpAddressThrottling }\n\n## Global per ip address throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalPerIpAddressThrottling`\n\n### Description\n\nEnforce global per ip address throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalThrottling }\n\n## Global throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalThrottling`\n\n### Description\n\nEnforce global throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLBackend }\n\n## GraphQL Composer\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLBackend`\n\n### Description\n\nThis plugin exposes a GraphQL API that you can compose with whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"schema\" : \"\\n type User {\\n name: String!\\n firstname: String!\\n }\\n\\n type Query {\\n users: [User] @json(data: \\\"[{ \\\\\\\"firstname\\\\\\\": \\\\\\\"Foo\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Bar\\\\\\\" }, { \\\\\\\"firstname\\\\\\\": \\\\\\\"Bar\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Foo\\\\\\\" }]\\\")\\n }\\n \",\n \"permissions\" : [ ],\n \"initial_data\" : null,\n \"max_depth\" : 15\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLProxy }\n\n## GraphQL Proxy\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLProxy`\n\n### Description\n\nThis plugin can apply validations (query, schema, max depth, max complexity) on graphql endpoints\n\n\n\n### Default configuration\n\n```json\n{\n \"endpoint\" : \"https://countries.trevorblades.com/graphql\",\n \"schema\" : null,\n \"max_depth\" : 50,\n \"max_complexity\" : 50000,\n \"path\" : \"/graphql\",\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLQuery }\n\n## GraphQL Query to REST\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLQuery`\n\n### Description\n\nThis plugin can be used to call GraphQL query endpoints and expose it as a REST endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://some.graphql/endpoint\",\n \"headers\" : { },\n \"method\" : \"POST\",\n \"query\" : \"{\\n\\n}\",\n \"timeout\" : 60000,\n \"response_path\" : null,\n \"response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GzipResponseCompressor }\n\n## Gzip compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GzipResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using gzip\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACCaller }\n\n## HMAC caller plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACCaller`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null,\n \"algo\" : \"HMAC-SHA512\",\n \"authorizationHeader\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACValidator }\n\n## HMAC access validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HeadersValidation }\n\n## Headers validation\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HeadersValidation`\n\n### Description\n\nThis plugin validates the values of incoming request headers\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Http3Switch }\n\n## Http3 traffic switch\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Http3Switch`\n\n### Description\n\nThis plugin injects additional alt-svc header to switch to the http3 server\n\n\n\n### Default configuration\n\n```json\n{\n \"ma\" : 3600\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ImageReplacer }\n\n## Image replacer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ImageReplacer`\n\n### Description\n\nReplace all response with content-type image/* as they are proxied\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://raw.githubusercontent.com/MAIF/otoroshi/master/resources/otoroshi-logo.png\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressAllowedList }\n\n## IP allowed list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressAllowedList`\n\n### Description\n\nThis plugin verifies the current request ip address is in the allowed list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressBlockList }\n\n## IP block list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressBlockList`\n\n### Description\n\nThis plugin verifies the current request ip address is not in the blocked list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQ }\n\n## JQ\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQ`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"request\" : \".\",\n \"response\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQRequest }\n\n## JQ transform request\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQRequest`\n\n### Description\n\nThis plugin let you transform request JSON body using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQResponse }\n\n## JQ transform response\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQResponse`\n\n### Description\n\nThis plugin let you transform JSON response using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlRequest }\n\n## request body json-to-xml\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlRequest`\n\n### Description\n\nThis plugin transform incoming request body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlResponse }\n\n## response body json-to-xml\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlResponse`\n\n### Description\n\nThis plugin transform response body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtSigner }\n\n## Jwt signer\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtSigner`\n\n### Description\n\nThis plugin can only generate token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"replace_if_present\" : true,\n \"fail_if_present\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerification }\n\n## Jwt verifiers\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerification`\n\n### Description\n\nThis plugin verifies the current request with one or more jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifiers\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerificationOnly }\n\n## Jwt verification only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerificationOnly`\n\n### Description\n\nThis plugin verifies the current request with one jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"fail_if_absent\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MaintenanceMode }\n\n## Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersIn }\n\n## Missing headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersIn`\n\n### Description\n\nThis plugin adds headers (if missing) in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersOut }\n\n## Missing headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersOut`\n\n### Description\n\nThis plugin adds headers (if missing) in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MockResponses }\n\n## Mock Responses\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MockResponses`\n\n### Description\n\nThis plugin returns mock responses\n\n\n\n### Default configuration\n\n```json\n{\n \"responses\" : [ ],\n \"pass_through\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MultiAuthModule }\n\n## Multi Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MultiAuthModule`\n\n### Description\n\nThis plugin applies an authentication module from a list of selected modules\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_modules\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleExpectedUser }\n\n## User logged in expected\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleExpectedUser`\n\n### Description\n\nThis plugin enforce that a user from any auth. module is logged in\n\n\n\n### Default configuration\n\n```json\n{\n \"only_from\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleUserExtractor }\n\n## User extraction from auth. module\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleUserExtractor`\n\n### Description\n\nThis plugin extracts users from an authentication module without enforcing login\n\n\n\n### Default configuration\n\n```json\n{\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitExtractor`\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitValidator }\n\n## Biscuit token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitValidator`\n\n### Description\n\nThis plugin validates a Biscuit token\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCertificateAsApikey }\n\n## Client certificate as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"read_only\" : false,\n \"allow_client_id_only\" : false,\n \"throttling_quota\" : 100,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000,\n \"constrained_services_only\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCertChainHeader }\n\n## Client certificate header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCertChainHeader`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers\n\n\n\n### Default configuration\n\n```json\n{\n \"send_pem\" : false,\n \"pem_header_name\" : \"X-Client-Cert-Pem\",\n \"send_dns\" : false,\n \"dns_header_name\" : \"X-Client-Cert-DNs\",\n \"send_chain\" : false,\n \"chain_header_name\" : \"X-Client-Cert-Chain\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentialTokenEndpoint }\n\n## Client credential token endpoint\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentialTokenEndpoint`\n\n### Description\n\nThis plugin provide the endpoint for the client_credential flow token endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentials }\n\n## Client Credential Service\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentials`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\",\n \"domain\" : \"*\",\n \"secure\" : true,\n \"biscuit\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomQuotas }\n\n## Custom quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomQuotas`\n\n### Description\n\nThis plugin will enforce quotas on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomThrottling }\n\n## Custom throttling\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomThrottling`\n\n### Description\n\nThis plugin will enforce throttling on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"throttling_quota\" : 100\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDefaultRequestBody }\n\n## Default request body\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDefaultRequestBody`\n\n### Description\n\nThis plugin adds a default request body if none specified\n\n\n\n### Default configuration\n\n```json\n{\n \"bodyBinary\" : \"\",\n \"contentType\" : \"text/plain\",\n \"contentEncoding\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDeferPlugin }\n\n## Defer Responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\n\n\n### Default configuration\n\n```json\n{\n \"duration\" : 0\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationSink`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoveryTargetsSelector`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgErrorRewriter }\n\n## Error response rewrite\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgErrorRewriter`\n\n### Description\n\nThis plugin catch http response with specific statuses and rewrite the response\n\n\n\n### Default configuration\n\n```json\n{\n \"ranges\" : [ {\n \"from\" : 500,\n \"to\" : 599\n } ],\n \"templates\" : {\n \"default\" : \"\\n \\n

An error occurred with id: ${error_id}

\\n

please contact your administrator with this error id !

\\n \\n\"\n },\n \"log\" : true,\n \"export\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoEndpoint`\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint `/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoHeader }\n\n## Geolocation header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasAllowedUsersValidator }\n\n## Allowed users only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\n\n\n### Default configuration\n\n```json\n{\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"email_domains\" : [ ],\n \"metadata_match\" : [ ],\n \"metadata_not_match\" : [ ],\n \"profile_match\" : [ ],\n \"profile_not_match\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator`\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following fetched from an http endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingValidator }\n\n## Client certificate matching\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertValidator }\n\n## Client Certificate Only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertValidator`\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHtmlPatcher }\n\n## Html Patcher\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"append_head\" : [ ],\n \"append_body\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHttpClientCache }\n\n## HTTP Client Cache\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHttpClientCache`\n\n### Description\n\nThis plugin add cache headers to responses\n\n\n\n### Default configuration\n\n```json\n{\n \"max_age_seconds\" : 86400,\n \"methods\" : [ \"GET\" ],\n \"status\" : [ 200 ],\n \"mime_types\" : [ \"text/html\" ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"apikey\" : null,\n \"timeout\" : 2000,\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Canary }\n\n## Izanami V1 Canary Campaign\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Canary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test)\n\n\n\n### Default configuration\n\n```json\n{\n \"experiment_id\" : \"foo:bar:qix\",\n \"config_id\" : \"foo:bar:qix:config\",\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 5000,\n \"route_config\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Proxy }\n\n## Izanami v1 APIs Proxy\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Proxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"/api/izanami\",\n \"feature_pattern\" : \"*\",\n \"config_pattern\" : \"*\",\n \"auto_context\" : false,\n \"features_enabled\" : true,\n \"features_with_context_enabled\" : true,\n \"configuration_enabled\" : false,\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 500\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgJwtUserExtractor }\n\n## Jwt user extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgJwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : \"none\",\n \"strict\" : true,\n \"strip\" : false,\n \"name_path\" : null,\n \"email_path\" : null,\n \"meta_path\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyApikeyCall }\n\n## Legacy apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyApikeyCall`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass. This plugin behaves exactly like the service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyAuthModuleCall }\n\n## Legacy Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyAuthModuleCall`\n\n### Description\n\nThis plugin applies an authentication module the same way service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLog4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLog4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"body\" : \"\",\n \"parse_body\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"global\",\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgResponseCache }\n\n## Response Cache\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\n\n\n### Default configuration\n\n```json\n{\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgSecurityTxt }\n\n## Security Txt\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgSecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/)\n\n\n\n### Default configuration\n\n```json\n{\n \"contact\" : \"contact@foo.bar\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgServiceQuotas }\n\n## Public quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current route\n\n\n\n### Default configuration\n\n```json\n{\n \"throttling_quota\" : 10000000,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgTrafficMirroring }\n\n## Traffic Mirroring\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgTrafficMirroring`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\n\n\n### Default configuration\n\n```json\n{\n \"to\" : \"https://foo.bar.dev\",\n \"enabled\" : true,\n \"capture_response\" : false,\n \"generate_events\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentExtractor }\n\n## User-Agent details extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentExtractor`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoEndpoint`\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint: /.well-known/otoroshi/plugins/user-agent\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoHeader }\n\n## User-Agent header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth1Caller }\n\n## OAuth1 caller\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"consumerKey\" : null,\n \"consumerSecret\" : null,\n \"token\" : null,\n \"tokenSecret\" : null,\n \"algo\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth2Caller }\n\n## OAuth2 caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"client_credentials\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : null,\n \"audience\" : null,\n \"user\" : null,\n \"password\" : null,\n \"cacheTokenSeconds\" : 600000,\n \"tlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCHeaders }\n\n## OIDC headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"profile\" : {\n \"send\" : false,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idToken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accessToken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiChallenge }\n\n## Otoroshi challenge token\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiChallenge`\n\n### Description\n\nThis plugin adds a jwt challenge token to the request to a backend and expects a response with a matching token\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"V2\",\n \"ttl\" : 30,\n \"request_header_name\" : null,\n \"response_header_name\" : null,\n \"algo_to_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"algo_from_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"state_resp_leeway\" : 10\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiHeadersIn }\n\n## Otoroshi headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiHeadersIn`\n\n### Description\n\nThis plugin adds Otoroshi specific headers to the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiInfos }\n\n## Otoroshi info. token\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiInfos`\n\n### Description\n\nThis plugin adds a jwt token with informations about the caller to the backend\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"Latest\",\n \"ttl\" : 30,\n \"header_name\" : null,\n \"add_fields\" : null,\n \"algo\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OverrideHost }\n\n## Override host header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OverrideHost`\n\n### Description\n\nThis plugin override the current Host header with the Host of the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.PublicPrivatePaths }\n\n## Public/Private paths\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.PublicPrivatePaths`\n\n### Description\n\nThis plugin allows or forbid request based on path patterns\n\n\n\n### Default configuration\n\n```json\n{\n \"strict\" : false,\n \"private_patterns\" : [ ],\n \"public_patterns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.QueryTransformer }\n\n## Query param transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.QueryTransformer`\n\n### Description\n\nThis plugin can modify the query params of the request\n\n\n\n### Default configuration\n\n```json\n{\n \"remove\" : [ ],\n \"rename\" : { },\n \"add\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RBAC }\n\n## RBAC\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RBAC`\n\n### Description\n\nThis plugin check if current user/apikey/jwt token has the right role\n\n\n\n### Default configuration\n\n```json\n{\n \"allow\" : [ ],\n \"deny\" : [ ],\n \"allow_all\" : false,\n \"deny_all\" : false,\n \"jwt_path\" : null,\n \"apikey_path\" : null,\n \"user_path\" : null,\n \"role_prefix\" : null,\n \"roles\" : \"roles\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ReadOnlyCalls }\n\n## Read only requests\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ReadOnlyCalls`\n\n### Description\n\nThis plugin verifies the current request only reads data\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Redirection }\n\n## Redirection\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Redirection`\n\n### Description\n\nThis plugin redirects the current request elsewhere\n\n\n\n### Default configuration\n\n```json\n{\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersIn }\n\n## Remove headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersIn`\n\n### Description\n\nThis plugin removes headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersOut }\n\n## Remove headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersOut`\n\n### Description\n\nThis plugin removes headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Robots }\n\n## Robots\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Robots`\n\n### Description\n\nThis plugin provides all the necessary tool to handle search engine robots\n\n\n\n### Default configuration\n\n```json\n{\n \"robot_txt_enabled\" : true,\n \"robot_txt_content\" : \"User-agent: *\\nDisallow: /\\n\",\n \"meta_enabled\" : true,\n \"meta_content\" : \"noindex,nofollow,noarchive\",\n \"header_enabled\" : true,\n \"header_content\" : \"noindex, nofollow, noarchive\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RoutingRestrictions }\n\n## Routing Restrictions\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RoutingRestrictions`\n\n### Description\n\nThis plugin apply routing restriction `method domain/path` on the current request/route\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_last\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"not_found\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.S3Backend }\n\n## S3 Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.S3Backend`\n\n### Description\n\nThis plugin is able to S3 bucket with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"bucket\" : \"\",\n \"endpoint\" : \"\",\n \"region\" : \"eu-west-1\",\n \"access\" : \"client\",\n \"secret\" : \"secret\",\n \"key\" : \"\",\n \"chunkSize\" : 8388608,\n \"v4auth\" : true,\n \"writeEvery\" : 60000,\n \"acl\" : \"private\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SOAPAction }\n\n## SOAP action\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SOAPAction`\n\n### Description\n\nThis plugin is able to call SOAP actions and expose it as a rest endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : null,\n \"envelope\" : \"\",\n \"action\" : null,\n \"preserve_query\" : true,\n \"charset\" : null,\n \"jq_request_filter\" : null,\n \"jq_response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SendOtoroshiHeadersBack }\n\n## Send otoroshi headers back\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SendOtoroshiHeadersBack`\n\n### Description\n\nThis plugin adds response header containing useful informations about the current call\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SnowMonkeyChaos }\n\n## Snow Monkey Chaos\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SnowMonkeyChaos`\n\n### Description\n\nThis plugin introduce some chaos into you life\n\n\n\n### Default configuration\n\n```json\n{\n \"large_request_fault\" : null,\n \"large_response_fault\" : null,\n \"latency_injection_fault\" : null,\n \"bad_responses_fault\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticBackend }\n\n## Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticBackend`\n\n### Description\n\nThis plugin is able to serve a static folder with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"root_path\" : \"/tmp\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticResponse }\n\n## Static Response\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticResponse`\n\n### Description\n\nThis plugin returns static responses\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"headers\" : { },\n \"body\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TailscaleSelectTargetByName }\n\n## Tailscale select target by name\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TailscaleSelectTargetByName`\n\n### Description\n\nThis plugin selects a machine instance on Tailscale network based on its name\n\n\n\n### Default configuration\n\n```json\n{\n \"machine_name\" : \"my-machine\",\n \"use_ip_address\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TcpTunnel }\n\n## TCP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TcpTunnel`\n\n### Description\n\nThis plugin creates TCP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.UdpTunnel }\n\n## UDP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.UdpTunnel`\n\n### Description\n\nThis plugin creates UDP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.W3CTracing }\n\n## W3C Trace Context\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.W3CTracing`\n\n### Description\n\nThis plugin propagates W3C Trace Context spans and can export it to Jaeger or Zipkin\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"noop\",\n \"endpoint\" : \"http://localhost:3333/spans\",\n \"timeout\" : 30000,\n \"baggage\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmAccessValidator }\n\n## Wasm Access control\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmAccessValidator`\n\n### Description\n\nDelegate route access to a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmBackend }\n\n## Wasm Backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmBackend`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as backend\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmOPA }\n\n## Open Policy Agent (OPA)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmOPA`\n\n### Description\n\nRepo policies as WASM modules\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : true,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmPreRoute }\n\n## Wasm pre-route\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmPreRoute`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as in pre-route phase\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRequestTransformer }\n\n## Wasm Request Transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRequestTransformer`\n\n### Description\n\nTransform the content of the request with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmResponseTransformer }\n\n## Wasm Response Transformer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmResponseTransformer`\n\n### Description\n\nTransform the content of a response with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouteMatcher }\n\n## Wasm Route Matcher\n\n### Defined on steps\n\n - `MatchRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouteMatcher`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as route matcher\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouter }\n\n## Wasm Router\n\n### Defined on steps\n\n - `Router`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouter`\n\n### Description\n\nCan decide for routing with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmSink }\n\n## Wasm Sink\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmSink`\n\n### Description\n\nHandle unmatched requests with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XForwardedHeaders }\n\n## X-Forwarded-* headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XForwardedHeaders`\n\n### Description\n\nThis plugin adds all the X-Forwarded-* headers to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonRequest }\n\n## request body xml-to-json\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonRequest`\n\n### Description\n\nThis plugin transform incoming request body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonResponse }\n\n## response body xml-to-json\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonResponse`\n\n### Description\n\nThis plugin transform response body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ZipFileBackend }\n\n## Zip file backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ZipFileBackend`\n\n### Description\n\nServes content from a zip file\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://github.com/MAIF/otoroshi/releases/download/16.11.2/otoroshi-manual-16.11.2.zip\",\n \"headers\" : { },\n \"dir\" : \"./zips\",\n \"prefix\" : null,\n \"ttl\" : 3600000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.tunnel.TunnelPlugin }\n\n## Remote tunnel calls\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.tunnel.TunnelPlugin`\n\n### Description\n\nThis plugin can contact remote service using tunnels\n\n\n\n### Default configuration\n\n```json\n{\n \"tunnel_id\" : \"default\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.wasm.proxywasm.NgCorazaWAF }\n\n## Coraza WAF\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.wasm.proxywasm.NgCorazaWAF`\n\n### Description\n\nCoraza WAF plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"ref\" : \"none\"\n}\n```\n\n\n\n\n\n@@@\n\n"},{"name":"create-plugins.md","id":"/plugins/create-plugins.md","url":"/plugins/create-plugins.html","title":"Create plugins","content":"# Create plugins\n\n@@@ warning\nThis section is under rewrite. The following content is deprecated\n@@@\n\nWhen everything has failed and you absolutely need a feature in Otoroshi to make your use case work, there is a solution. Plugins is the feature in Otoroshi that allow you to code how Otoroshi should behave when receiving, validating and routing an http request. With request plugin, you can change request / response headers and request / response body the way you want, provide your own apikey, etc.\n\n## Plugin types\n\nthere are many plugin types explained @ref:[here](./plugins.md) \n\n## Code and signatures\n\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/requestsink.scala#L14-L19\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/routing.scala#L75-L78\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/accessvalidator.scala#L65-L85\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/script.scala#269-L540\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/eventlistener.scala#L27-L48\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L69-L164\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L108-L110\n\n\nfor more information about APIs you can use\n\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#package\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.mvc.Results\n* https://github.com/MAIF/otoroshi\n* https://doc.akka.io/docs/akka/2.5/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/scaladsl/Source.html\n\n## Plugin examples\n\n@ref:[A lot of plugins](./built-in-plugins.md) comes with otoroshi, you can find them on [github](https://github.com/MAIF/otoroshi/tree/master/otoroshi/app/plugins)\n\n## Writing a plugin from Otoroshi UI\n\nLog into Otoroshi and go to `Settings (cog icon) / Plugins`. Here you can create multiple request transformer scripts and associate it with service descriptor later.\n\n@@@ div { .centered-img }\n\n@@@\n\nwhen you write for instance a transformer in the Otoroshi UI, do the following\n\n```scala\nimport akka.stream.Materializer\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass MyTransformer extends RequestTransformer {\n\n val logger = Logger(\"my-transformer\")\n\n // implements the methods you want\n}\n\n// WARN: do not forget this line to provide a working instance of your transformer to Otoroshi\nnew MyTransformer()\n```\n\nYou can use the compile button to check if the script compiles, or code the transformer in your IDE (see next point).\n\nThen go to a service descriptor, scroll to the bottom of the page, and select your transformer in the list\n\n@@@ div { .centered-img }\n\n@@@\n\n## Providing a transformer from Java classpath\n\nYou can write your own transformer using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"request-transformer-example\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1.x.x\"\n )\n```\n\n@@@ warning\nyou MUST provide plugins that lies in the `otoroshi_plugins` package or in a sub-package of `otoroshi_plugins`. If you do not, your plugin will not be found by otoroshi. for example\n\n```scala\npackage otoroshi_plugins.com.my.company.myplugin\n```\n\nalso you don't have to instantiate your plugin at the end of the file like in the Otoroshi UI\n@@@\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/transformer.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in your service descriptor, you can chose your transformer in the list. If you want to do it from the API, you have to defined the transformerRef using `cp:` prefix like \n\n```json\n{\n \"transformerRef\": \"cp:otoroshi_plugins.my.class.package.MyTransformer\"\n}\n```\n\n## Getting custom configuration from the Otoroshi config. file\n\nLet say you need to provide custom configuration values for a script, then you can customize a configuration file of Otoroshi\n\n```hocon\ninclude \"application.conf\"\n\notoroshi {\n scripts {\n enabled = true\n }\n}\n\nmy-transformer {\n env = \"prod\"\n maxRequestBodySize = 2048\n maxResponseBodySize = 2048\n}\n```\n\nthen start Otoroshi like\n\n```sh\njava -Dconfig.file=/path/to/custom.conf -jar otoroshi.jar\n```\n\nthen, in your transformer, you can write something like \n\n```scala\npackage otoroshi_plugins.com.example.otoroshi\n\nimport akka.stream.Materializer\nimport akka.stream.scaladsl._\nimport akka.util.ByteString\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass BodyLengthLimiter extends RequestTransformer {\n\n override def def transformResponseWithCtx(ctx: TransformerResponseContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxResponseBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n\n override def transformRequestWithCtx(ctx: TransformerRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxRequestBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n}\n```\n\n## Using a library that is not embedded in Otoroshi\n\nJust use the `classpath` option when running Otoroshi\n\n```sh\njava -cp \"/path/to/library.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nBe carefull as your library can conflict with other libraries used by Otoroshi and affect its stability\n\n## Enabling plugins\n\nplugins can be enabled per service from the service settings page or globally from the danger zone in the plugins section.\n\n## Full example\n\na full external plugin example can be found @link:[here](https://github.com/mathieuancelin/otoroshi-wasmer-plugin)\n"},{"name":"index.md","id":"/plugins/index.md","url":"/plugins/index.html","title":"Otoroshi plugins","content":"# Otoroshi plugins\n\nIn this sections, you will find informations about Otoroshi plugins system\n\n* @ref:[Plugins system](./plugins.md)\n* @ref:[Create plugins](./create-plugins.md)\n* @ref:[Built in plugins](./built-in-plugins.md)\n* @ref:[Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@ index\n\n* [Plugins system](./plugins.md)\n* [Create plugins](./create-plugins.md)\n* [Built in plugins](./built-in-plugins.md)\n* [Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@"},{"name":"plugins.md","id":"/plugins/plugins.md","url":"/plugins/plugins.html","title":"Otoroshi plugins system","content":"# Otoroshi plugins system\n\nOtoroshi includes several extension points that allows you to create your own plugins and support stuff not supported by default.\n\n## Kind of available plugins\n\n@@@ div { .plugin-kind }\n###Request Sink\n\nUsed when no services are matched in Otoroshi. Can reply with any content.\n@@@\n\n@@@ div { .plugin-kind }\n###Pre routing\n\nUsed to extract values (like custom apikeys) and provide them to other plugins or Otoroshi engine\n@@@\n\n@@@ div { .plugin-kind }\n###Access Validator\n\nUsed to validate if a request can pass or not based on whatever you want\n@@@\n\n@@@ div { .plugin-kind }\n###Request Transformer\n\nUsed to transform request, responses and their body. Can be used to return arbitrary content\n@@@\n\n@@@ div { .plugin-kind }\n###Event listener\n\nAny plugin type can listen to Otoroshi internal events and react to thems\n@@@\n\n@@@ div { .plugin-kind }\n###Job\n\nTasks that can run automatically once, on be scheduled with a cron expression or every defined interval\n@@@\n\n@@@ div { .plugin-kind }\n###Exporter\n\nUsed to export events and Otoroshi alerts to an external source\n@@@\n\n@@@ div { .plugin-kind }\n###Request handler\n\nUsed to handle traffic without passing through Otoroshi routing and apply own rules\n@@@\n\n@@@ div { .plugin-kind }\n###Nano app\n\nUsed to write an api directly in Otoroshi in Scala language\n@@@"},{"name":"search.md","id":"/search.md","url":"/search.html","title":"Search otoroshi documentation","content":"# Search otoroshi documentation\n\n
\n\n"},{"name":"anonymous-reporting.md","id":"/topics/anonymous-reporting.md","url":"/topics/anonymous-reporting.html","title":"Anonymous reporting","content":"# Anonymous reporting\n\nThe best way of supporting us in Otoroshi developement is to enable Anonymous reporting.\n\n## Details\n\nWhen this feature is active, Otoroshi perdiodically send anonymous information about its configuration.\n\nThis information helps us to know how Otoroshi is used, it's a precious hint to prioritise our roadmap.\n\nBelow is an example of what is send by Otoroshi. You can find more information about these fields either on @ref:[entities documentation](../entities/index.md) or [by reading the source code](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/jobs/reporting.scala#L174-L458).\n\n```json\n{\n \"@timestamp\": 1679514537259,\n \"timestamp_str\": \"2023-03-22T20:48:57.259+01:00\",\n \"@id\": \"4edb54171-8156-4947-b821-41d6c2bd1ba7\",\n \"otoroshi_cluster_id\": \"1148aee35-a487-47b0-b494-a2a44862c618\",\n \"otoroshi_version\": \"16.0.0-dev\",\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"datastore\": \"file\",\n \"env\": \"dev\",\n \"features\": {\n \"snow_monkey\": false,\n \"clever_cloud\": false,\n \"kubernetes\": false,\n \"elastic_read\": true,\n \"lets_encrypt\": false,\n \"auto_certs\": false,\n \"wasm_manager\": true,\n \"backoffice_login\": false\n },\n \"stats\": {\n \"calls\": 3823,\n \"data_in\": 480406,\n \"data_out\": 4698261,\n \"rate\": 0,\n \"duration\": 35.89899494949495,\n \"overhead\": 24.696984848484846,\n \"data_in_rate\": 0,\n \"data_out_rate\": 0,\n \"concurrent_requests\": 0\n },\n \"engine\": {\n \"uses_new\": true,\n \"uses_new_full\": false\n },\n \"cluster\": {\n \"mode\": \"Leader\",\n \"all_nodes\": 1,\n \"alive_nodes\": 1,\n \"leaders_count\": 1,\n \"workers_count\": 0,\n \"nodes\": [\n {\n \"id\": \"node_15ac62ec3-3e0d-48c1-a8ea-15de97088e3c\",\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"version\": \"16.0.0-dev\",\n \"type\": \"Leader\",\n \"cpu_usage\": 10.992902320605205,\n \"load_average\": 44.38720703125,\n \"heap_used\": 527,\n \"heap_size\": 2048,\n \"relay\": true,\n \"tunnels\": 0\n }\n ]\n },\n \"entities\": {\n \"scripts\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"routes\": {\n \"count\": 24,\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"router_routes\": {\n \"count\": 27,\n \"http_clients\": {\n \"ahc\": 25,\n \"akka\": 2,\n \"netty\": 0,\n \"akka_ws\": 0\n },\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"route_compositions\": {\n \"count\": 1,\n \"plugins\": {\n \"min\": 1,\n \"max\": 1,\n \"avg\": 1\n },\n \"by_kind\": {\n \"global\": 1\n }\n },\n \"apikeys\": {\n \"count\": 6,\n \"by_kind\": {\n \"disabled\": 0,\n \"with_rotation\": 0,\n \"with_read_only\": 0,\n \"with_client_id_only\": 0,\n \"with_constrained_services\": 0,\n \"with_meta\": 2,\n \"with_tags\": 1\n },\n \"authorized_on\": {\n \"min\": 1,\n \"max\": 4,\n \"avg\": 2\n }\n },\n \"jwt_verifiers\": {\n \"count\": 6,\n \"by_strategy\": {\n \"pass_through\": 6\n },\n \"by_alg\": {\n \"HSAlgoSettings\": 6\n }\n },\n \"certificates\": {\n \"count\": 9,\n \"by_kind\": {\n \"auto_renew\": 6,\n \"exposed\": 6,\n \"client\": 1,\n \"keypair\": 1\n }\n },\n \"auth_modules\": {\n \"count\": 8,\n \"by_kind\": {\n \"basic\": 7,\n \"oauth2\": 1\n }\n },\n \"service_descriptors\": {\n \"count\": 3,\n \"plugins\": {\n \"old\": 0,\n \"new\": 0\n },\n \"by_kind\": {\n \"disabled\": 1,\n \"fault_injection\": 0,\n \"health_check\": 1,\n \"gzip\": 0,\n \"jwt\": 0,\n \"cors\": 1,\n \"auth\": 0,\n \"protocol\": 0,\n \"restrictions\": 0\n }\n },\n \"teams\": {\n \"count\": 2\n },\n \"tenants\": {\n \"count\": 2\n },\n \"service_groups\": {\n \"count\": 2\n },\n \"data_exporters\": {\n \"count\": 10,\n \"by_kind\": {\n \"elastic\": 5,\n \"file\": 2,\n \"metrics\": 1,\n \"console\": 1,\n \"s3\": 1\n }\n },\n \"otoroshi_admins\": {\n \"count\": 5,\n \"by_kind\": {\n \"simple\": 2,\n \"webauthn\": 3\n }\n },\n \"backoffice_sessions\": {\n \"count\": 1,\n \"by_kind\": {\n \"simple\": 1\n }\n },\n \"private_apps_sessions\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"tcp_services\": {\n \"count\": 0\n }\n },\n \"plugins_usage\": {\n \"cp:otoroshi.next.plugins.AdditionalHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.DisableHttp10\": 2,\n \"cp:otoroshi.next.plugins.OverrideHost\": 27,\n \"cp:otoroshi.next.plugins.TailscaleFetchCertificate\": 1,\n \"cp:otoroshi.next.plugins.OtoroshiInfos\": 6,\n \"cp:otoroshi.next.plugins.MissingHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.Redirection\": 2,\n \"cp:otoroshi.next.plugins.OtoroshiChallenge\": 5,\n \"cp:otoroshi.next.plugins.BuildMode\": 2,\n \"cp:otoroshi.next.plugins.XForwardedHeaders\": 2,\n \"cp:otoroshi.next.plugins.NgLegacyAuthModuleCall\": 2,\n \"cp:otoroshi.next.plugins.Cors\": 4,\n \"cp:otoroshi.next.plugins.OtoroshiHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.NgDefaultRequestBody\": 1,\n \"cp:otoroshi.next.plugins.NgHttpClientCache\": 1,\n \"cp:otoroshi.next.plugins.ReadOnlyCalls\": 2,\n \"cp:otoroshi.next.plugins.RemoveHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.JwtVerificationOnly\": 1,\n \"cp:otoroshi.next.plugins.ApikeyCalls\": 3,\n \"cp:otoroshi.next.plugins.WasmAccessValidator\": 3,\n \"cp:otoroshi.next.plugins.WasmBackend\": 3,\n \"cp:otoroshi.next.plugins.IpAddressAllowedList\": 2,\n \"cp:otoroshi.next.plugins.AuthModule\": 4,\n \"cp:otoroshi.next.plugins.RemoveHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.IpAddressBlockList\": 2,\n \"cp:otoroshi.next.proxy.ProxyEngine\": 1,\n \"cp:otoroshi.next.plugins.JwtVerification\": 3,\n \"cp:otoroshi.next.plugins.GzipResponseCompressor\": 2,\n \"cp:otoroshi.next.plugins.SendOtoroshiHeadersBack\": 3,\n \"cp:otoroshi.next.plugins.AdditionalHeadersIn\": 4,\n \"cp:otoroshi.next.plugins.SOAPAction\": 1,\n \"cp:otoroshi.next.plugins.NgLegacyApikeyCall\": 6,\n \"cp:otoroshi.next.plugins.ForceHttpsTraffic\": 2,\n \"cp:otoroshi.next.plugins.NgErrorRewriter\": 1,\n \"cp:otoroshi.next.plugins.MissingHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.MaintenanceMode\": 3,\n \"cp:otoroshi.next.plugins.RoutingRestrictions\": 2,\n \"cp:otoroshi.next.plugins.HeadersValidation\": 2\n }\n}\n```\n\n## Toggling\n\nAnonymous reporting can be toggled any time using :\n\n- the UI (Features > Danger zone > Send anonymous reports)\n- `otoroshi.anonymous-reporting.enabled` configuration\n- `OTOROSHI_ANONYMOUS_REPORTING_ENABLED` env variable\n"},{"name":"chaos-engineering.md","id":"/topics/chaos-engineering.md","url":"/topics/chaos-engineering.html","title":"Chaos engineering with the Snow Monkey","content":"# Chaos engineering with the Snow Monkey\n\nNihonzaru (the Snow Monkey) is the chaos engineering tool provided by Otoroshi. You can access it at `Settings (cog icon) / Snow Monkey`.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Chaos engineering\n\nOtoroshi offers some tools to introduce [chaos engineering](https://principlesofchaos.org/) in your everyday life. With chaos engineering, you will improve the resilience of your architecture by creating faults in production on running systems. With [Nihonzaru (the snow monkey)](https://en.wikipedia.org/wiki/Japanese_macaque) Otoroshi helps you to create faults on http request/response handled by Otoroshi. \n\n@@@ div { .centered-img }\n\n@@@\n\n## Settings\n\n@@@ div { .centered-img }\n\n@@@\n\nThe snow monkey let you define a few settings to work properly :\n\n* **Include user facing apps.**: you want to create fault in production, but maybe you don't want your users to enjoy some nice snow monkey generated error pages. Using this switch let you include of not user facing apps (ui apps). Each service descriptor has a `User facing app switch` that will be used by the snow monkey.\n* **Dry run**: when dry run is enabled, outages will be registered and will generate events and alerts (in the otoroshi eventing system) but requests won't be actualy impacted. It's a good way to prepare applications to the snow monkey habits\n* **Outage strategy**: Either `AllServicesPerGroup` or `OneServicePerGroup`. It means that only one service per group or all services per groups will have `n` outages (see next bullet point) during the snow monkey working period\n* **Outages per day**: during snow monkey working period, each service per group or one service per group will have only `n` outages registered \n* **Working period**: the snow monkey only works during a working period. Here you can defined when it starts and when it stops\n* **Outage duration**: here you can defined the bounds for the random outage duration when an outage is created on a service\n* **Impacted groups**: here you can define a list of service groups impacted by the snow monkey. If none is specified, then all service groups will be impacted\n\n## Faults\n\nWith the snow monkey, you can generate four types of faults\n\n* **Large request fault**: Add trailing bytes at the end of the request body (if one)\n* **Large response fault**: Add trailing bytes at the end of the response body\n* **Latency injection fault**: Add random response latency between two bounds\n* **Bad response injection fault**: Create predefined responses with custom headers, body and status code\n\nEach fault let you define a ratio for impacted requests. If you specify a ratio of `0.2`, then 20% of the requests for the impacte service will be impacted by this fault\n\n@@@ div { .centered-img }\n\n@@@\n\nThen you juste have to start the snow monkey and enjoy the show ;)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Current outages\n\nIn the last section of the snow monkey page, you can see current outages (per service), when they started, their duration, etc ...\n\n@@@ div { .centered-img }\n\n@@@"},{"name":"dev-portal.md","id":"/topics/dev-portal.md","url":"/topics/dev-portal.html","title":"Developer portal with Daikoku","content":"# Developer portal with Daikoku\n\nWhile Otoroshi is the perfect tool to manage your webapps in a technical point of view it lacked of business perspective. This is not the case anymore with Daikoku.\n\nWhile Otoroshi is a standalone, Daikoku is a developer portal which stands in front of Otoroshi and provides some business feature.\n\nWhether you want to use Daikoku for your public APIs, you want to monetize or with your private APIs to provide some documentation, facilitation and self-service feature, it will be the perfect portal for Otoroshi.\n\n@@@div { .plugin .platform }\n## Daikoku\n\nRun your first Daikoku with a simple jar or with one Docker command.\n\n\n
\nTry Daikoku \n
\n@link:[With jar](https://maif.github.io/daikoku/devmanual/getdaikoku/frombinaries.html)\n@link:[With Docker](https://maif.github.io/daikoku/devmanual/getdaikoku/fromdocker.html)\n@@@\n\n@@@div { .plugin .platform }\n## Contribute\n\nDaikoku is opensource, so all contributions are welcome.\n\n\n@link:[Show the repository](https://github.com/MAIF/daikoku)\n@@@\n\n@@@div { .plugin .platform }\n## Documentation\n\nDaikoku and its UI are fully documented.\n\n\n@link:[Read the documentation](https://maif.github.io/daikoku/devmanual/)\n@@@\n\n"},{"name":"engine.md","id":"/topics/engine.md","url":"/topics/engine.html","title":"Proxy engine","content":"# Proxy engine\n\nStarting from the `1.5.3` release, otoroshi offers a new plugin that implements the next generation of the proxy engine. \nThis engine has been designed based on our 5 years experience building, maintaining and running the previous one.\nIt tries to fix all the drawback we may have encountered during those years and highly improve performances, user experience, reporting and debugging capabilities. \n\nThe new engine is fully plugin oriented in order to spend CPU cycles only on useful stuff. You can enable this plugin only on some domain names so you can easily A/B test the new engine. The new proxy engine is designed to be more reactive and more efficient generally. It is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nStarting from version `16.0.0`, this engine will be enabled by default on any new otoroshi cluster. In a future version, the engine will be enabled for any new or exisiting otoroshi cluster.\n\n## Enabling the new engine\n\nBy default, all freshly started Otoroshi instances have the new proxy engine enabled by default, for the other, to enable the new proxy engine on an otoroshi instance, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration, enable it and in `domains` add the values of the desired domains (let say we want to use the new engine on `api.foo.bar`. It is possible to use `*.foo.bar` if that's what you want to do).\n\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the previous one.\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"debug_headers\" : false,\n \"reporting\": true,\n \"domains\" : [ \"api.foo.bar\" ],\n \"deny_domains\" : [ ],\n }\n}\n```\n\nif you need to enable global plugin with the new engine, you can add the following configuration in the `global plugins` configuration object \n\n```javascript\n{\n ...\n \"ng\": {\n \"slots\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.W3CTracing\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"baggage\": {\n \"foo\": \"bar\"\n }\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.RequestSinkWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.apikeys.ClientCredentialService\",\n \"ClientCredentialService\": {\n \"domain\": \"ccs-next-gen.oto.tools\",\n \"expiration\": 3600000,\n \"defaultKeyPair\": \"otoroshi-jwt-signing\",\n \"secure\": false\n }\n }\n }\n ]\n }\n ...\n}\n```\n\n## Entities\n\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `backends`: a list of targets to contact a backend\n\n## Entities sync\n\nA new behavior introduced for the new proxy engine is the entities sync job. To avoid unecessary operations on the underlying datastore when routing requests, a new job has been setup in otoroshi that synchronize the content of the datastore (at least a part of it) with an in-memory cache. Because of it, the propagation of changes between an admin api call and the actual result on routing can be longer than before. When a node creates, updates, or deletes an entity via the admin api, other nodes need to wait for the next poll to purge the old cached entity and start using the new one. You can change the interval between syncs with the configuration key `otoroshi.next.state-sync-interval` or the env. variable `OTOROSHI_NEXT_STATE_SYNC_INTERVAL`. The default value is `10000` and the unit is `milliseconds`\n\n@@@ warning\nBecause of entities sync, memory consumption of otoroshi will be significantly higher than previous versions. You can use `otoroshi.next.monitor-proxy-state-size=true` config (or `OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE` env. variable) to monitor the actual memory size of the entities cache. This will produce the `ng-proxy-state-size-monitoring` metric in standard otoroshi metrics\n@@@\n\n## Automatic conversion\n\nThe new engine uses new entities for its configuration, but in order to facilitate transition between the old world and the new world, all the `service descriptors` of an otoroshi instance are automatically converted live into `routes` periodically. Any `service descriptor` should still work as expected through the new engine while enjoying all the perks.\n\n@@@ warning\nthe experimental nature of the engine can imply unexpected behaviors for converted service descriptors\n@@@\n\n## Routing\n\nthe new proxy engine introduces a new router that has enhanced capabilities and performances. The router can handle thousands of routes declarations without compromising performances.\n\nThe new route allow routes to be matched on a combination of\n\n* hostname\n* path\n* header values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n* query param values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n\npatch matching works \n\n* exactly\n * matches `/api/foo` with `/api/foo` and not with `/api/foo/bar`\n* starting with value (default behavior, like the previous engine)\n * matches `/api/foo` with `/api/foo` but also with `/api/foo/bar`\n\npath matching can also include wildcard paths and even path params\n\n* plain old path: `subdomain.domain.tld/api/users`\n* wildcard path: `subdomain.domain.tld/api/users/*/bills`\n* named path params: `subdomain.domain.tld/api/users/:id/bills`\n* named regex path params: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n\nhostname matching works on \n\n* exact values\n * `subdomain.domain.tld`\n* wildcard values like\n * `*.domain.tld`\n * `subdomain.*.tld`\n\nas path matching can now include named path params, it is possible to perform a ful url rewrite on the target path like \n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Plugins\n\nthe new route entity defines a plugin pipline where any plugin can be enabled or not and can be active only on some paths. \nEach plugin slot in the pipeline holds the plugin id and the plugin configuration. \n\nYou can also enable debugging only on a plugin instance instead of the whole route (see [the debugging section](#debugging))\n\n```javascript\n{ \n ...\n \"plugins\" : [ {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.OverrideHost\",\n \"include\" : [ ],\n \"exclude\" : [ ],\n \"config\" : { }\n }, {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\" : [ ],\n \"exclude\" : [ \"/openapi.json\" ],\n \"config\" : { }\n } ]\n}\n```\n\nyou can find the list of built-in plugins @ref:[here](../plugins/built-in-plugins.md)\n\n## Using legacy plugins\n\nif you need to use legacy otoroshi plugins with the new engine, you can use several wrappers in order to do so\n\n* `otoroshi.next.plugins.wrappers.PreRoutingWrapper`\n* `otoroshi.next.plugins.wrappers.AccessValidatorWrapper`\n* `otoroshi.next.plugins.wrappers.RequestSinkWrapper`\n* `otoroshi.next.plugins.wrappers.RequestTransformerWrapper`\n* `otoroshi.next.plugins.wrappers.CompositeWrapper`\n\nto use it, just declare a plugin slot with the right wrapper and in the config, declare the `plugin` you want to use and its configuration like:\n\n```javascript\n{\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.PreRoutingWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.jwt.JwtUserExtractor\",\n \"JwtUserExtractor\": {\n \"verifier\" : \"$ref\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\": \"email\",\n \"metaPath\" : null\n }\n }\n}\n```\n\n## Reporting\n\nby default, any request hiting the new engine will generate an execution report with informations about how the request pipeline steps were performed. It is possible to export those reports as `RequestFlowReport` events using classical data exporter. By default, exporting for reports is not enabled, you must enable the `export_reporting` flag on a `route` or `service`.\n\n```javascript\n{\n \"@id\": \"8efac472-07bc-4a80-8d27-4236309d7d01\",\n \"@timestamp\": \"2022-02-15T09:51:25.402+01:00\",\n \"@type\": \"RequestFlowReport\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"service_548f13bb-a809-4b1d-9008-fae3b1851092\",\n \"@service\": \"demo-service\",\n \"@env\": \"prod\",\n \"route\": {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"report\": {\n \"id\" : \"ab73707b3-946b-4853-92d4-4c38bbaac6d6\",\n \"creation\" : \"2022-02-15T09:51:25.402+01:00\",\n \"termination\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 5,\n \"duration_ns\" : 5905522,\n \"overhead\" : 4,\n \"overhead_ns\" : 4223215,\n \"overhead_in\" : 2,\n \"overhead_in_ns\" : 2687750,\n \"overhead_out\" : 1,\n \"overhead_out_ns\" : 1535465,\n \"state\" : \"Successful\",\n \"steps\" : [ {\n \"task\" : \"start-handling\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 177430,\n \"ctx\" : null\n }, {\n \"task\" : \"check-concurrent-requests\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 145242,\n \"ctx\" : null\n }, {\n \"task\" : \"find-route\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 497119,\n \"ctx\" : {\n \"found_route\" : {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"matched_path\" : \"\",\n \"exact\" : true,\n \"params\" : { },\n \"matched_routes\" : [ \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\" ]\n }\n }, {\n \"task\" : \"compute-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 105151,\n \"ctx\" : {\n \"disabled_plugins\" : [ ],\n \"filtered_plugins\" : [ ]\n }\n }, {\n \"task\" : \"tenant-check\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 26097,\n \"ctx\" : null\n }, {\n \"task\" : \"check-global-maintenance\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 14132,\n \"ctx\" : null\n }, {\n \"task\" : \"call-before-request-callbacks\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 56671,\n \"ctx\" : null\n }, {\n \"task\" : \"extract-tracking-id\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 5207,\n \"ctx\" : null\n }, {\n \"task\" : \"call-pre-route-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 39786,\n \"ctx\" : null\n }, {\n \"task\" : \"call-access-validator-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 25311,\n \"ctx\" : null\n }, {\n \"task\" : \"enforce-global-limits\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 296617,\n \"ctx\" : {\n \"remaining_quotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 10000000,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 10000000,\n \"remainingCallsPerDay\" : 10000000,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 10000000,\n \"remainingCallsPerMonth\" : 10000000\n }\n }\n }, {\n \"task\" : \"choose-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 368899,\n \"ctx\" : {\n \"backend\" : {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n }\n }, {\n \"task\" : \"transform-request\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 506363,\n \"ctx\" : null\n }, {\n \"task\" : \"call-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 2,\n \"duration_ns\" : 2163470,\n \"ctx\" : null\n }, {\n \"task\" : \"transform-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 279887,\n \"ctx\" : null\n }, {\n \"task\" : \"stream-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 382952,\n \"ctx\" : null\n }, {\n \"task\" : \"trigger-analytics\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 812036,\n \"ctx\" : null\n }, {\n \"task\" : \"request-success\",\n \"start\" : 1644915085408,\n \"start_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 0,\n \"ctx\" : null\n } ]\n }\n}\n```\n\n## Debugging\n\nwith the new reporting capabilities, the new engine also have debugging capabilities built in. In you enable the `debug_flow` flag on a route (or service), the resulting `RequestFlowReport` will be enriched with contextual informations between each plugins of the route plugin pipeline\n\n@@@ note\nyou can also use the `Try it` feature of the new route designer UI to get debug reports automatically for a specific call\n@@@\n\n## HTTP traffic capture\n\nusing the `capture` flag, a `TrafficCaptureEvent` is generated for each http request/response. This event will contains request and response body. Those events can be exported using @ref:[data exporters](../entities/data-exporters.md) as usual. You can also use the @ref:[GoReplay file exporter](../entities/data-exporters.md#goreplay-file) that is specifically designed to ingest those events and create [GoReplay](https://goreplay.org/) files (`.gor`)\n\n@@@ warning\nthis feature can have actual impact on CPU and RAM consumption\n@@@\n\n```json\n{\n \"@id\": \"d5998b0c4-cb08-43e6-9921-27472c7a56e0\",\n \"@timestamp\": 1651828801115,\n \"@type\": \"TrafficCaptureEvent\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"@service\": \"test-server\",\n \"@env\": \"prod\",\n \"route\": {\n \"id\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"name\": \"test-server\"\n },\n \"request\": {\n \"id\": \"152250645825034725600000\",\n \"int_id\": 115,\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"test-server-next-gen.oto.tools:9999\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\",\n \"Remote-Address\": \"127.0.0.1:57660\",\n \"Timeout-Access\": \"\",\n \"Raw-Request-URI\": \"/\",\n \"Tls-Session-Info\": \"Session(1651828041285|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"path\": \"/\",\n \"domain\": null,\n \"http_only\": true,\n \"max_age\": null,\n \"secure\": false,\n \"same_site\": null\n }\n ],\n \"tls\": false,\n \"uri\": \"/\",\n \"path\": \"/\",\n \"version\": \"HTTP/1.1\",\n \"has_body\": true,\n \"remote\": \"127.0.0.1\",\n \"client_cert_chain\": null,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_request\": {\n \"url\": \"http://localhost:3000/\",\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"localhost\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\"\n },\n \"version\": \"HTTP/1.1\",\n \"client_cert_chain\": null,\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"domain\": null,\n \"path\": \"/\",\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": true\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"int_id\": 33,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_response\": {\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"response\": {\n \"id\": \"152250645825034725600000\",\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"user-agent-details\": null,\n \"origin-details\": null,\n \"instance-number\": 0,\n \"instance-name\": \"dev\",\n \"instance-zone\": \"local\",\n \"instance-region\": \"local\",\n \"instance-dc\": \"local\",\n \"instance-provider\": \"local\",\n \"instance-rack\": \"local\",\n \"cluster-mode\": \"Leader\",\n \"cluster-name\": \"otoroshi-leader-9hnv5HUXpbCZD7Ee\"\n}\n```\n\n## openapi import\n\nas the new router offers possibility to match exactly on a single path and a single method, and with the help of the `service` entity, it is now pretty easy to import openapi document as `route-compositions` entities. To do that, a new api has been made available to perform the translation. Be aware that this api **DOES NOT** save the entity and just return the result of the translation. \n\n```sh\ncurl -X POST \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/route-compositions/_openapi' \\\n -d '{\"domain\":\"oto-api-proxy.oto.tools\",\"openapi\":\"https://raw.githubusercontent.com/MAIF/otoroshi/master/otoroshi/public/openapi.json\"}'\n```\n\n@@@ div { .centered-img }\n\n@@@\n\n"},{"name":"events-and-analytics.md","id":"/topics/events-and-analytics.md","url":"/topics/events-and-analytics.html","title":"Events and analytics","content":"# Events and analytics\n\nOtoroshi is a solution fully traced : calls to services, access to UI, creation of resources, etc.\n\n@@@ warning\nYou have to use [Elastic](https://www.elastic.co) to enable analytics features in Otoroshi\n@@@\n\n## Events\n\n* Analytics event\n* Gateway event\n* TCP event\n* Healthcheck event\n\n## Event log\n\nOtoroshi can read his own exported events from an Elasticsearch instance, set up in the danger zone. Theses events are available from the UI, at the following route: `https://xxxxx/bo/dashboard/events`.\n\nThe `Global events` page display all events of **GatewayEvent** type. This page is a way to quickly read an interval of events and can be used in addition of a Kibana instance.\n\nFor each event, a list of information will be displayed and an additional button `content` to watch the full content of the event, at the JSON format. \n\n## Alerts \n\n* `MaxConcurrentRequestReachedAlert`: happening when the handled requests number are greater than the limit of concurrent requests indicated in the global configuration of Otoroshi\n* `CircuitBreakerOpenedAlert`: happening when the circuit breaker pass from closed to opened\n* `CircuitBreakerClosedAlert`: happening when the circuit breaker pass from opened to closed\n* `SessionDiscardedAlert`: send when an admin discarded an admin sessions\n* `SessionsDiscardedAlert`: send when an admin discarded all admin sessions\n* `PanicModeAlert`: send when panic mode is enabled\n* `OtoroshiExportAlert`: send when otoroshi global configuration is exported\n* `U2FAdminDeletedAlert`: send when an admin has deleted an other admin user\n* `BlackListedBackOfficeUserAlert`: send when a blacklisted user has tried to acccess to the UI\n* `AdminLoggedInAlert`: send when an user admin has logged to the UI\n* `AdminFirstLogin`: send when an user admin has successfully logged to the UI for the first time\n* `AdminLoggedOutAlert`: send when an user admin has logged out from Otoroshi\n* `GlobalConfigModification`: send when an user amdin has changed the global configuration of Otoroshi\n* `RevokedApiKeyUsageAlert`: send when an user admin has revoked an apikey\n* `ServiceGroupCreatedAlert`: send when an user admin has created a service group\n* `ServiceGroupUpdatedAlert`: send when an user admin has updated a service group\n* `ServiceGroupDeletedAlert`: send when an user admin has deleted a service group\n* `ServiceCreatedAlert`: send when an user admin has created a tcp service\n* `ServiceUpdatedAlert`: send when an user admin has updated a tcp service\n* `ServiceDeletedAlert`: send when an user admin has deleted a tcp service\n* `ApiKeyCreatedAlert`: send when an user admin has crated a new apikey\n* `ApiKeyUpdatedAlert`: send when an user admin has updated a new apikey\n* `ApiKeyDeletedAlert`: send when an user admin has deleted a new apikey\n\n## Audit\n\nWith Otoroshi, any admin action and any sucpicious/alert action is recorded. These records are stored in Otoroshi’s datastore (only the last n records, defined by the `otoroshi.events.maxSize` config key). All the records can be send through the analytics mechanism (WebHook, Kafka, Elastic) for external and/or further usage. We recommand sending away those records for security reasons.\n\nOtoroshi keep the following list of information for each executed action:\n\n* `Date`: moment of the action\n* `User`: name of the owner\n* `From`: IP of the concerned user\n* `Action`: action performed by the person. The possible actions are:\n\n * `ACCESS_APIKEY`: User accessed a apikey\n * `ACCESS_ALL_APIKEYS`: User accessed all apikeys\n * `CREATE_APIKEY`: User created a apikey\n * `UPDATE_APIKEY`: User updated a apikey\n * `DELETE_APIKEY`: User deleted a apikey\n * `ACCESS_AUTH_MODULE`: User accessed an Auth. module\n * `ACCESS_ALL_AUTH_MODULES`: User accessed all Auth. modules\n * `CREATE_AUTH_MODULE`: User created an Auth. module\n * `UPDATE_AUTH_MODULE`: User updated an Auth. module\n * `DELETE_AUTH_MODULE`: User deleted an Auth. module\n * `ACCESS_CERTIFICATE`: User accessed a certificate\n * `ACCESS_ALL_CERTIFICATES`: User accessed all certificates\n * `CREATE_CERTIFICATE`: User created a certificate\n * `UPDATE_CERTIFICATE`: User updated a certificate\n * `DELETE_CERTIFICATE`: User deleted a certificate\n * `ACCESS_CLIENT_CERT_VALIDATOR`: User accessed a client cert. validator\n * `ACCESS_ALL_CLIENT_CERT_VALIDATORS`: User accessed all client cert. validators\n * `CREATE_CLIENT_CERT_VALIDATOR`: User created a client cert. validator\n * `UPDATE_CLIENT_CERT_VALIDATOR`: User updated a client cert. validator\n * `DELETE_CLIENT_CERT_VALIDATOR`: User deleted a client cert. validator\n * `ACCESS_DATA_EXPORTER_CONFIG`: User accessed a data exporter config\n * `ACCESS_ALL_DATA_EXPORTER_CONFIG`: User accessed all data exporter config\n * `CREATE_DATA_EXPORTER_CONFIG`: User created a data exporter config\n * `UPDATE_DATA_EXPORTER_CONFIG`: User updated a data exporter config\n * `DELETE_DATA_EXPORTER_CONFIG`: User deleted a data exporter config\n * `ACCESS_GLOBAL_JWT_VERIFIER`: User accessed a global jwt verifier\n * `ACCESS_ALL_GLOBAL_JWT_VERIFIERS`: User accessed all global jwt verifiers\n * `CREATE_GLOBAL_JWT_VERIFIER`: User created a global jwt verifier\n * `UPDATE_GLOBAL_JWT_VERIFIER`: User updated a global jwt verifier\n * `DELETE_GLOBAL_JWT_VERIFIER`: User deleted a global jwt verifier\n * `ACCESS_SCRIPT`: User accessed a script\n * `ACCESS_ALL_SCRIPTS`: User accessed all scripts\n * `CREATE_SCRIPT`: User created a script\n * `UPDATE_SCRIPT`: User updated a script\n * `DELETE_SCRIPT`: User deleted a Script\n * `ACCESS_SERVICES_GROUP`: User accessed a service group\n * `ACCESS_ALL_SERVICES_GROUPS`: User accessed all services groups\n * `CREATE_SERVICE_GROUP`: User created a service group\n * `UPDATE_SERVICE_GROUP`: User updated a service group\n * `DELETE_SERVICE_GROUP`: User deleted a service group\n * `ACCESS_SERVICES_FROM_SERVICES_GROUP`: User accessed all services from a services group\n * `ACCESS_TCP_SERVICE`: User accessed a tcp service\n * `ACCESS_ALL_TCP_SERVICES`: User accessed all tcp services\n * `CREATE_TCP_SERVICE`: User created a tcp service\n * `UPDATE_TCP_SERVICE`: User updated a tcp service\n * `DELETE_TCP_SERVICE`: User deleted a tcp service\n * `ACCESS_TEAM`: User accessed a Team\n * `ACCESS_ALL_TEAMS`: User accessed all teams\n * `CREATE_TEAM`: User created a team\n * `UPDATE_TEAM`: User updated a team\n * `DELETE_TEAM`: User deleted a team\n * `ACCESS_TENANT`: User accessed a Tenant\n * `ACCESS_ALL_TENANTS`: User accessed all tenants\n * `CREATE_TENANT`: User created a tenant\n * `UPDATE_TENANT`: User updated a tenant\n * `DELETE_TENANT`: User deleted a tenant\n * `SERVICESEARCH`: User searched for a service\n * `ACTIVATE_PANIC_MODE`: Admin activated panic mode\n\n\n* `Message`: explicit message about the action (example: the `SERVICESEARCH` action happened when an `user searched for a service`)\n* `Content`: all information at JSON format\n\n## Global metrics\n\nThe global metrics are displayed on the index page of the Otoroshi UI. Otoroshi provides information about :\n\n* the number of requests served\n* the amount of data received and sended\n* the number of concurrent requests\n* the number of requests per second\n* the current overhead\n\nMore metrics can be found on the **Global analytics** page (available at https://xxxxxx/bo/dashboard/stats).\n\n## Monitoring services\n\nOnce you have declared services, you can monitor them with Otoroshi. \n\nLet's starting by setup Otoroshi to push events to an elastic cluster via a data exporter. Then you will can setup Otoroshi events read from an elastic cluster. Go to `settings (cog icon) / Danger Zone` and expand the `Analytics: Elastic cluster (read)` section.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service healthcheck\n\nIf you have defined an health check URL in the service descriptor, you can access the health check page from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service live stats\n\nYou can also monitor live stats like total of served request, average response time, average overhead, etc. The live stats page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service analytics\n\nYou can also get some aggregated metrics. The analytics page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n## New proxy engine\n\n### Debug reporting\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `debug_flow` flag, events of type `RequestFlowReport` are generated\n\n### Traffic capture\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `capture` flag, events of type `TrafficCaptureEvent` are generated. It contains everything that compose otoroshi input http request and output http responses\n"},{"name":"expression-language.md","id":"/topics/expression-language.md","url":"/topics/expression-language.html","title":"Expression language","content":"# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```"},{"name":"graphql-composer.md","id":"/topics/graphql-composer.md","url":"/topics/graphql-composer.html","title":"GraphQL Composer Plugin","content":"# GraphQL Composer Plugin\n\n
\nRoute plugins:\nGraphQL Composer\n
\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\n> GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.\n[Official GraphQL website](https://graphql.org/)\n\nAPIs RESTful and GraphQL development has become one of the most popular activities for companies as well as users in recent times. In fast scaling companies, the multiplication of clients can cause the number of API needs to grow at scale.\n\nOtoroshi comes with a solution to create and meet your customers' needs without constantly creating and recreating APIs: the `GraphQL composer plugin`. The GraphQL Composer is an useful plugin to build an GraphQL API from multiples differents sources. These sources can be REST apis, GraphQL api or anything that supports the HTTP protocol. In fact, the plugin can define and expose for each of your client a specific GraphQL schema, which only corresponds to the needs of the customers.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n## Tutorial\n\nLet's take an example to get a better view of this plugin. We want to build a schema with two types: \n\n* an user with a name and a password \n* an country with a name and its users.\n\nTo build this schema, we need to use three custom directives. A `directive` decorates part of a GraphQL schema or operation with additional configuration. Directives are preceded by the @ character, like so:\n\n* @ref:[rest](#directives) : to call a http rest service with dynamic path params\n* @ref:[permission](#directives) : to restrict the access to the sensitive field\n* @ref:[graphql](#directives) : to call a graphQL service by passing a url and the associated query\n\nThe final schema of our tutorial should look like this\n```graphql\ntype Country {\n name: String\n users: [User] @rest(url: \"http://localhost:5000/countries/${item.name}/users\")\n}\n\ntype User {\n name: String\n password: String @password(value: \"ADMIN\")\n}\n\ntype Query {\n users: [User] @rest(url: \"http://localhost:5000/users\", paginate: true)\n user(id: String): User @rest(url: \"http://localhost:5000/users/${params.id}\")\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com\", query: \"{ countries { name }}\", paginate: true)\n}\n```\n\nNow you know the GraphQL Composer basics and how it works, let's configure it on our project:\n\n* create a route using the new Otoroshi router describing the previous countries API\n* add the GraphQL composer plugin\n* configure the plugin with the schema\n* try to call it\n\n@@@ div { .centered-img }\n\n@@@\n\n### Setup environment\n\nFirst of all, we need to download the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v1.5.15/otoroshi.jar'\n```\n\nNow, just run the command belows to start the Otoroshi, and look the console to see the output.\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow, login to [the UI](http://otoroshi.oto.tools:8080) with \n```sh\nuser = admin@otoroshi.io\npassword = password\n```\n\n### Create our countries API\n\nFirst thing to do in any new API is of course creating a `route`. We need 4 informations which are:\n\n* name: `My countries API`\n* frontend: exposed on `countries-api.oto.tools`\n* plugins: the list of plugins with only the `GraphQL composer` plugin\n\nLet's make a request call through the Otoroshi Admin API (with the default apikey), like the example below\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\"countries.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\"\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\n### Build the countries API \n\nLet's continue our API by patching the configuration of the GraphQL plugin with the complete schema.\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/api/routes/countries-api' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\n \"countries.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\",\n \"config\": {\n \"schema\": \"type Country {\\n name: String\\n users: [User] @rest(url: \\\"http://localhost:8181/countries/${item.name}/users\\\", headers: \\\"{}\\\")\\n}\\n\\ntype Query {\\n users: [User] @rest(url: \\\"http://localhost:8181/users\\\", paginate: true, headers: \\\"{}\\\")\\n user(id: String): User @rest(url: \\\"http://localhost:8181/users/${params.id}\\\")\\n countries: [Country] @graphql(url: \\\"https://countries.trevorblades.com\\\", query: \\\"{ countries { name }}\\\", paginate: true)\\ntype User {\\n name: String\\n password: String }\\n\"\n }\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThe route is created but it expects an API, exposed on the localhost:8181, to work. \n\nLet's create this simple API which returns a list of users and of countries. This should look like the following snippet.\nThe API uses express as http server.\n\n```js\nconst express = require('express')\n\nconst app = express()\n\nconst users = [\n {\n name: 'Joe',\n password: 'password'\n },\n {\n name: 'John',\n password: 'password2'\n }\n]\n\nconst countries = [\n {\n name: 'Andorra',\n users: [users[0]]\n },\n {\n name: 'United Arab Emirates',\n users: [users[1]]\n }\n]\n\napp.get('/users', (_, res) => {\n return res.json(users)\n})\n\napp.get(`/users/:name`, (req, res) => {\n res.json(users.find(u => u.name === req.params.name))\n})\n\napp.get('/countries/:id/users', (req, res) => {\n const country = countries.find(c => c.name === req.params.id)\n\n if (country) \n return res.json(country.users)\n else \n return res.json([])\n})\n\napp.listen(8181, () => {\n console.log(`Listening on 8181`)\n});\n\n```\n\nLet's try to make a first call to our countries API.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": [\n { \n \"name\":\"Andorra\",\n \"users\": [\n { \"name\":\"Joe\" }\n ]\n }\n ]\n }\n}\n```\n\nThe call graph should looks like\n\n```\n1. Calls https://countries.trevorblades.com\n2. For each country:\n - extract the field name\n - calls http://localhost:8181/countries/${country}/users to get the list of users for this country\n```\n\nYou may have noticed that we added an argument at the end of the graphql directive named `paginate`. It enabled the paging for the client accepting limit and offset parameters. These parameters are used by the plugin to filter and reduce the content.\n\nLet's make a new call that does not accept any country.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": []\n }\n}\n```\n\nLet's move on to the next section to secure sensitive field of our API.\n\n### Basics of permissions \n\nThe permission directives has been created to protect the fields of the graphql schema. The validation process starts by create a `context` for all incoming requests, based on the list of paths defined in the permissions field of the plugin. The permissions paths can refer to the request data (url, headers, etc), user credentials (api key, etc) and informations about the matched route. Then the process can validate that the value or values are present in the `context`.\n\n@@@div { .simple-block }\n\n
\nPermission\n\n
\n\n*Arguments : value and unauthorized_value*\n\nThe permission directive can be used to secure a field on **one** value. The directive checks that a specific value is present in the `context`.\n\nTwo arguments are available, the first, named `value`, is required and designates the value found. The second optional value, `unauthorized_value`, can be used to indicates, in the outcoming response, the rejection message.\n\n**Example**\n```js\ntype User {\n id: String @permission(\n value: \"FOO\", \n unauthorized_value: \"You're not authorized to get this field\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nAll permissions\n\n
\n\n*Arguments : values and unauthorized_value*\n\nThis directive is presumably the same as the previous one except that it takes a list of values.\n\n**Example**\n```js\ntype User {\n id: String @allpermissions(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO and BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nOne permissions of\n\n
\n*Arguments : values and unauthorized_value*\n\nThis directive takes a list of values and validate that one of them is in the context.\n\n**Example**\n```js\ntype User {\n id: String @onePermissionsOf(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO or BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nAuthorize\n\n
\n\n*Arguments : path, value and unauthorized_value*\n\nThe authorize directive has one more required argument, named `path`, which indicates the path to value, in the context. Unlike the last three directives, the authorize directive doesn't search in the entire context but at the specified path.\n\n**Example**\n```js\ntype User {\n id: String @authorize(\n path: \"$.raw_request.headers.foo\", \n value: \"BAR\", \n unauthorized_value: \"Bar could not be found in the foo header\")\n}\n```\n@@@\n\nLet's restrict the password field to the users that comes with a `role` header of the value `ADMIN`.\n\n1. Patch the configuration of the API by adding the permissions in the configuration of the plugin.\n```json\n...\n \"permissions\": [\"$.raw_request.headers.role\"]\n...\n```\n\n1. Add an directive on the password field in the schema\n```graphql\ntype User {\n name: String\n password: String @permission(value: \"ADMIN\")\n}\n```\n\nLet's make a call with the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: ADMIN'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nNow try to change the value of the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: USER'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nThe error message should look like \n\n```json\n{\n \"errors\": [\n {\n \"message\": \"You're not authorized\",\n \"path\": [\n \"countries\",\n 0,\n \"users\",\n 0,\n \"password\"\n ],\n ...\n }\n ]\n}\n```\n\n\n# Glossary\n\n## Directives\n\n@@@div { .simple-block }\n\n
\nRest\n\n
\n\n*Arguments : url, method, headers, timeout, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to expose servers that communicate using the http protocol. The only required argument is the `url`.\n\n**Example**\n```js\ntype Query {\n users(limit: Int, offset: Int): [User] @rest(url: \"http://foo.oto.tools/users\", method: \"GET\")\n}\n```\n\nIt can be placed on the field of a query and type. To custom your url queries, you can use the path parameter and another field with respectively, `params` and `item` variables.\n\n**Example**\n```js\ntype Country {\n name: String\n phone: String\n users: [User] @rest(url: \"http://foo.oto.tools/users/${item.name}\")\n}\n\ntype Query {\n user(id: String): User @rest(url: \"http://foo.oto.tools/users/${params.id}\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nGraphQL\n\n
\n\n*Arguments : url, method, headers, timeout, query, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to call an other graphql server.\n\nThe required argument are the `url` and the `query`.\n\n**Example**\n```js\ntype Query {\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com/\", query: \"{ countries { name phone }}\")\n}\n\ntype Country {\n name: String\n phone: String\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nSoap\n\n
\n*Arguments: all following arguments*\n\nThe soap directive is used to call a soap service. \n\n```js\ntype Query {\n randomNumber: String @soap(\n jq_response_filter: \".[\\\"soap:Envelope\\\"] | .[\\\"soap:Body\\\"] | .[\\\"m:NumberToWordsResponse\\\"] | .[\\\"m:NumberToWordsResult\\\"]\", \n url: \"https://www.dataaccess.com/webservicesserver/numberconversion.wso\", \n envelope: \" \\n \\n \\n \\n 12 \\n \\n \\n\")\n}\n```\n\n\n##### Specific arguments\n\n| Argument | Type | Optional | Default value |\n| --------------------------- | --------- | -------- | ------------- |\n| envelope | *STRING* | Required | |\n| url | *STRING* | x | |\n| action | *STRING* | x | |\n| preserve_query | *BOOLEAN* | Required | true |\n| charset | *STRING* | x | |\n| convert_request_body_to_xml | *BOOLEAN* | Required | true |\n| jq_request_filter | *STRING* | x | |\n| jq_response_filter | *STRING* | x | |\n\n@@@\n\n@@@div { .simple-block }\n\n
\nJSON\n\n
\n*Arguments: path, json, paginate*\n\nThe json directive can be used to expose static data or mocked data. The first usage is to defined a raw stringify JSON in the `data` argument. The second usage is to set data in the predefined field of the GraphQL plugin composer and to specify a path in the `path` argument.\n\n**Example**\n```js\ntype Query {\n users_from_raw_data: [User] @json(data: \"[{\\\"firstname\\\":\\\"Foo\\\",\\\"name\\\":\\\"Bar\\\"}]\")\n users_from_predefined_data: [User] @json(path: \"users\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
\nMock\n\n
\n*Arguments: url*\n\nThe mock directive is to used with the Mock Responses Plugin, also named `Charlatan`. This directive can be interesting to mock your schema and start to use your Otoroshi route before starting to develop the underlying service.\n\n**Example**\n```js\ntype Query {\n users: @mock(url: \"/users\")\n}\n```\n\nThis example supposes that the Mock Responses plugin is set on the route's feed, and that an endpoint `/users` is available.\n\n@@@\n\n### List of directive arguments\n\n| Argument | Type | Optional | Default value |\n| ------------------ | ---------------- | --------------------------- | ------------- |\n| url | *STRING* | | |\n| method | *STRING* | x | GET |\n| headers | *STRING* | x | |\n| timeout | *INT* | x | 5000 |\n| data | *STRING* | x | |\n| path | *STRING* | x (only for json directive) | |\n| query | *STRING* | x | |\n| response_path | *STRING* | x | |\n| response_filter | *STRING* | x | |\n| limit | *INT* | x | |\n| offset | *INT* | x | |\n| value | *STRING* | | |\n| values | LIST of *STRING* | |\n| path | *STRING* | | |\n| paginate | *BOOLEAN* | x | |\n| unauthorized_value | *STRING* | x (only for permissions directive) | |\n"},{"name":"green-score.md","id":"/topics/green-score.md","url":"/topics/green-score.html","title":"Green Score","content":"# Green Score\n\nThe Green Score provide aggregated, quantitative data about the performance and behavior of an API over time. It is an aggregation of static and dynamic values that are coming from the usage of routes in Otoroshi. The main objective is to advise users on the consumption of their APIs and services.\n\n\n\nOtoroshi has a complete integration of the collective rules, divided into four concerns: **Architecture**, **Design**, **Usage** and **Logs retention**. The 6000 score points are spread over the four parts and a final note is given for each group of routes.\n\nThe API green score is available on 16.9.0 or later version of Otoroshi. You can find the feature on the search bar of your Otoroshi UI or directly in the sidebar by clicking on **Green score**.\n\nTo start the process, click on Add New Group, give a name and select a first route to audit. After clicking on the hammer icon, you can select the rules respected by your route. Before saving, you can adjust the values used to calculate the dynamic score. These thresholds are used to calculate a second green score depending on the amount of data you want not to exceed from your downstream service and the following other values: \n\n* **Overhead**: Otoroshi's calculation time to handle the request and response\n* **Duration**: the complete duration from the recpetion of the request by Otoroshi until the client gets a response\n* **Backend duration**: the time required for downstream service to respond to Otoroshi\n* **Calls**: the rate of calls by seconds\n* **Data in**: the amount of data received by the downstream service\n* **Data out**: the amount of data produced by the downstream service\n* **Headers in**: the amount of headers received by the downstream service\n* **Headers out**: the amount of headers produced by the downstream service\n\nThe Green Score works for all architectures, including simple leader or more advanced concept like [clustering](https://maif.github.io/otoroshi/manual/deploy/clustering.html)."},{"name":"http3.md","id":"/topics/http3.md","url":"/topics/http3.html","title":"HTTP3 support","content":"# HTTP3 support\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nHTTP3 server and client previews are available in otoroshi since version 1.5.14\n\n\n## Server\n\nto enable http3 server preview, you need to enable the following flags\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048\n```\n\nthen you will be able to send HTTP3 request on port 10048. For instance, using [quiche-client](https://github.com/cloudflare/quiche)\n\n```sh\ncargo run --bin quiche-client -- --no-verify 'https://my-service.oto.tools:10048'\n```\n\n## Client\n\nto consume services exposed with HTTP3, just select the `HTTP/3.0` protocol in the backend target."},{"name":"index.md","id":"/topics/index.md","url":"/topics/index.html","title":"Detailed topics","content":"# Detailed topics\n\nIn this sections, you will find informations about various Otoroshi topics \n\n* @ref:[Proxy engine](./engine.md)\n* @ref:[WASM support](./wasm-usage.md)\n* @ref:[Chaos engineering](./chaos-engineering.md)\n* @ref:[TLS](./tls.md)\n* @ref:[Otoroshi's PKI](./pki.md)\n* @ref:[Monitoring](./monitoring.md)\n* @ref:[Events and analytics](./events-and-analytics.md)\n* @ref:[Developer portal with Daikoku](./dev-portal.md)\n* @ref:[Sessions management](./sessions-mgmt.md)\n* @ref:[The Otoroshi communication protocol](./otoroshi-protocol.md)\n* @ref:[Expression language](./expression-language.md)\n* @ref:[Otoroshi user rights](./user-rights.md)\n* @ref:[GraphQL composer](./graphql-composer.md)\n* @ref:[Secret vaults](./secrets.md)\n* @ref:[Otoroshi tunnels](./tunnels.md)\n* @ref:[Relay routing](./relay-routing.md)\n* @ref:[Alternative http backend](./netty-server.md)\n* @ref:[HTTP3 support](./http3.md)\n* @ref:[Anonymous reporting](./anonymous-reporting.md)\n* @ref:[OpenTelemetry support](./opentelemetry.md)\n* @ref:[Green score](./green-score.md)\n\n@@@ index\n\n* [Proxy engine](./engine.md)\n* [WASM support](./wasm-usage.md)\n* [Chaos engineering](./chaos-engineering.md)\n* [TLS](./tls.md)\n* [Otoroshi's PKI](./pki.md)\n* [Monitoring](./monitoring.md)\n* [Events and analytics](./events-and-analytics.md)\n* [Developer portal with Daikoku](./dev-portal.md)\n* [Sessions management](./sessions-mgmt.md)\n* [The Otoroshi communication protocol](./otoroshi-protocol.md)\n* [Expression language](./expression-language.md)\n* [Otoroshi user rights](./user-rights.md)\n* [GraphQL composer](./graphql-composer.md)\n* [Secret vaults](./secrets.md)\n* [Otoroshi tunnels](./tunnels.md)\n* [Relay routing](./relay-routing.md)\n* [Alternative http backend](./netty-server.md)\n* [HTTP3 support](./http3.md)\n* [Anonymous reporting](./anonymous-reporting.md)\n* [OpenTelemetry support](./opentelemetry.md)\n* [Green score](./green-score.md)\n \n@@@\n"},{"name":"monitoring.md","id":"/topics/monitoring.md","url":"/topics/monitoring.html","title":"Monitoring","content":"# Monitoring\n\nThe Otoroshi API exposes two endpoints to know more about instance health. All the following endpoint are exposed on the instance host through it's ip address. It is also exposed on the otoroshi api hostname and the otoroshi backoffice hostname\n\n* `/health`: the health of the Otoroshi instance\n* `/metrics`: the metrics of the Otoroshi instance, either in JSON or Prometheus format using the `Accept` header (with `application/json` / `application/prometheus` values) or the `format` query param (with `json` or `prometheus` values)\n* `/live`: returns an http 200 response `{\"live\": true}` when the service is alive\n* `/ready`: return an http 200 response `{\"ready\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"ready\": false}`\n* `/startup`: return an http 200 response `{\"started\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"started\": false}`\n\nthose routes are also available on any hostname leading to otoroshi with a twist in the URL\n\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/health\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/metrics\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/live\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/ready\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/startup\n\n## Endpoints security\n\nThe two endpoints are exposed publicly on the Otoroshi admin api. But you can remove the corresponding public pattern and query the endpoints using standard apikeys. If you don't want to use apikeys but don't want to expose the endpoints publicly, you can defined two config. variables (`otoroshi.health.accessKey` or `HEALTH_ACCESS_KEY` and `otoroshi.metrics.accessKey` or `OTOROSHI_METRICS_ACCESS_KEY`) that will hold an access key for the endpoints. Then you can call the endpoints with an `access_key` query param with the value defined in the config. If you don't defined `otoroshi.metrics.accessKey` but define `otoroshi.health.accessKey`, `otoroshi.metrics.accessKey` will have the value of `otoroshi.health.accessKey`.\n \n## Examples\n\nlet say `otoroshi.health.accessKey` has value `MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY`\n\n```sh\n$ curl http://otoroshi-api.oto.tools:8080/health\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"otoroshi\":\"healthy\",\"datastore\":\"healthy\"}\n\n$ curl -H 'Accept: application/json' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"version\":\"4.0.0\",\"gauges\":{\"attr.app.commit\":{\"value\":\"xxxx\"},\"attr.app.id\":{\"value\":\"xxxx\"},\"attr.cluster.mode\":{\"value\":\"Leader\"},\"attr.cluster.name\":{\"value\":\"otoroshi-leader-0\"},\"attr.instance.env\":{\"value\":\"prod\"},\"attr.instance.id\":{\"value\":\"xxxx\"},\"attr.instance.number\":{\"value\":\"0\"},\"attr.jvm.cpu.usage\":{\"value\":136},\"attr.jvm.heap.size\":{\"value\":1409},\"attr.jvm.heap.used\":{\"value\":112},\"internals.0.concurrent-requests\":{\"value\":1},\"internals.global.throttling-quotas\":{\"value\":2},\"jvm.attr.name\":{\"value\":\"2085@xxxx\"},\"jvm.attr.uptime\":{\"value\":2296900},\"jvm.attr.vendor\":{\"value\":\"JDK11\"},\"jvm.gc.PS-MarkSweep.count\":{\"value\":3},\"jvm.gc.PS-MarkSweep.time\":{\"value\":261},\"jvm.gc.PS-Scavenge.count\":{\"value\":12},\"jvm.gc.PS-Scavenge.time\":{\"value\":161},\"jvm.memory.heap.committed\":{\"value\":1477967872},\"jvm.memory.heap.init\":{\"value\":1690304512},\"jvm.memory.heap.max\":{\"value\":3005218816},\"jvm.memory.heap.usage\":{\"value\":0.03916456777568639},\"jvm.memory.heap.used\":{\"value\":117698096},\"jvm.memory.non-heap.committed\":{\"value\":166445056},\"jvm.memory.non-heap.init\":{\"value\":7667712},\"jvm.memory.non-heap.max\":{\"value\":994050048},\"jvm.memory.non-heap.usage\":{\"value\":0.1523920694986979},\"jvm.memory.non-heap.used\":{\"value\":151485344},\"jvm.memory.pools.CodeHeap-'non-nmethods'.committed\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.max\":{\"value\":5832704},\"jvm.memory.pools.CodeHeap-'non-nmethods'.usage\":{\"value\":0.28408093398876405},\"jvm.memory.pools.CodeHeap-'non-nmethods'.used\":{\"value\":1656960},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.committed\":{\"value\":11796480},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.usage\":{\"value\":0.09536102872567315},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.used\":{\"value\":11721088},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.committed\":{\"value\":37355520},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.usage\":{\"value\":0.2538573047187417},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.used\":{\"value\":31202304},\"jvm.memory.pools.Compressed-Class-Space.committed\":{\"value\":14942208},\"jvm.memory.pools.Compressed-Class-Space.init\":{\"value\":0},\"jvm.memory.pools.Compressed-Class-Space.max\":{\"value\":367001600},\"jvm.memory.pools.Compressed-Class-Space.usage\":{\"value\":0.033858838762555805},\"jvm.memory.pools.Compressed-Class-Space.used\":{\"value\":12426248},\"jvm.memory.pools.Metaspace.committed\":{\"value\":99794944},\"jvm.memory.pools.Metaspace.init\":{\"value\":0},\"jvm.memory.pools.Metaspace.max\":{\"value\":375390208},\"jvm.memory.pools.Metaspace.usage\":{\"value\":0.25168142904782426},\"jvm.memory.pools.Metaspace.used\":{\"value\":94478744},\"jvm.memory.pools.PS-Eden-Space.committed\":{\"value\":349700096},\"jvm.memory.pools.PS-Eden-Space.init\":{\"value\":422576128},\"jvm.memory.pools.PS-Eden-Space.max\":{\"value\":1110966272},\"jvm.memory.pools.PS-Eden-Space.usage\":{\"value\":0.07505125052077188},\"jvm.memory.pools.PS-Eden-Space.used\":{\"value\":83379408},\"jvm.memory.pools.PS-Eden-Space.used-after-gc\":{\"value\":0},\"jvm.memory.pools.PS-Old-Gen.committed\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.init\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.max\":{\"value\":2253914112},\"jvm.memory.pools.PS-Old-Gen.usage\":{\"value\":0.014950035505168354},\"jvm.memory.pools.PS-Old-Gen.used\":{\"value\":33696096},\"jvm.memory.pools.PS-Old-Gen.used-after-gc\":{\"value\":23791152},\"jvm.memory.pools.PS-Survivor-Space.committed\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.init\":{\"value\":70254592},\"jvm.memory.pools.PS-Survivor-Space.max\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.usage\":{\"value\":0.59375},\"jvm.memory.pools.PS-Survivor-Space.used\":{\"value\":622592},\"jvm.memory.pools.PS-Survivor-Space.used-after-gc\":{\"value\":622592},\"jvm.memory.total.committed\":{\"value\":1644412928},\"jvm.memory.total.init\":{\"value\":1697972224},\"jvm.memory.total.max\":{\"value\":3999268864},\"jvm.memory.total.used\":{\"value\":269184904},\"jvm.thread.blocked.count\":{\"value\":0},\"jvm.thread.count\":{\"value\":82},\"jvm.thread.daemon.count\":{\"value\":11},\"jvm.thread.deadlock.count\":{\"value\":0},\"jvm.thread.deadlocks\":{\"value\":[]},\"jvm.thread.new.count\":{\"value\":0},\"jvm.thread.runnable.count\":{\"value\":25},\"jvm.thread.terminated.count\":{\"value\":0},\"jvm.thread.timed_waiting.count\":{\"value\":10},\"jvm.thread.waiting.count\":{\"value\":47}},\"counters\":{},\"histograms\":{},\"meters\":{},\"timers\":{}}\n\n$ curl -H 'Accept: application/prometheus' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n# TYPE attr_jvm_cpu_usage gauge\nattr_jvm_cpu_usage 83.0\n# TYPE attr_jvm_heap_size gauge\nattr_jvm_heap_size 1409.0\n# TYPE attr_jvm_heap_used gauge\nattr_jvm_heap_used 220.0\n# TYPE internals_0_concurrent_requests gauge\ninternals_0_concurrent_requests 1.0\n# TYPE internals_global_throttling_quotas gauge\ninternals_global_throttling_quotas 3.0\n# TYPE jvm_attr_uptime gauge\njvm_attr_uptime 2372614.0\n# TYPE jvm_gc_PS_MarkSweep_count gauge\njvm_gc_PS_MarkSweep_count 3.0\n# TYPE jvm_gc_PS_MarkSweep_time gauge\njvm_gc_PS_MarkSweep_time 261.0\n# TYPE jvm_gc_PS_Scavenge_count gauge\njvm_gc_PS_Scavenge_count 12.0\n# TYPE jvm_gc_PS_Scavenge_time gauge\njvm_gc_PS_Scavenge_time 161.0\n# TYPE jvm_memory_heap_committed gauge\njvm_memory_heap_committed 1.477967872E9\n# TYPE jvm_memory_heap_init gauge\njvm_memory_heap_init 1.690304512E9\n# TYPE jvm_memory_heap_max gauge\njvm_memory_heap_max 3.005218816E9\n# TYPE jvm_memory_heap_usage gauge\njvm_memory_heap_usage 0.07680553268571043\n# TYPE jvm_memory_heap_used gauge\njvm_memory_heap_used 2.30817432E8\n# TYPE jvm_memory_non_heap_committed gauge\njvm_memory_non_heap_committed 1.66510592E8\n# TYPE jvm_memory_non_heap_init gauge\njvm_memory_non_heap_init 7667712.0\n# TYPE jvm_memory_non_heap_max gauge\njvm_memory_non_heap_max 9.94050048E8\n# TYPE jvm_memory_non_heap_usage gauge\njvm_memory_non_heap_usage 0.15262878997416435\n# TYPE jvm_memory_non_heap_used gauge\njvm_memory_non_heap_used 1.51720656E8\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_nmethods__committed 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_nmethods__max 5832704.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_nmethods__usage 0.28408093398876405\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_nmethods__used 1656960.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__committed 1.1862016E7\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__usage 0.09610562183417755\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__used 1.1812608E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__committed 3.735552E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__usage 0.25493618368435084\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__used 3.1334912E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_committed gauge\njvm_memory_pools_Compressed_Class_Space_committed 1.4942208E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_init gauge\njvm_memory_pools_Compressed_Class_Space_init 0.0\n# TYPE jvm_memory_pools_Compressed_Class_Space_max gauge\njvm_memory_pools_Compressed_Class_Space_max 3.670016E8\n# TYPE jvm_memory_pools_Compressed_Class_Space_usage gauge\njvm_memory_pools_Compressed_Class_Space_usage 0.03386023385184152\n# TYPE jvm_memory_pools_Compressed_Class_Space_used gauge\njvm_memory_pools_Compressed_Class_Space_used 1.242676E7\n# TYPE jvm_memory_pools_Metaspace_committed gauge\njvm_memory_pools_Metaspace_committed 9.9794944E7\n# TYPE jvm_memory_pools_Metaspace_init gauge\njvm_memory_pools_Metaspace_init 0.0\n# TYPE jvm_memory_pools_Metaspace_max gauge\njvm_memory_pools_Metaspace_max 3.75390208E8\n# TYPE jvm_memory_pools_Metaspace_usage gauge\njvm_memory_pools_Metaspace_usage 0.25170985813247426\n# TYPE jvm_memory_pools_Metaspace_used gauge\njvm_memory_pools_Metaspace_used 9.4489416E7\n# TYPE jvm_memory_pools_PS_Eden_Space_committed gauge\njvm_memory_pools_PS_Eden_Space_committed 3.49700096E8\n# TYPE jvm_memory_pools_PS_Eden_Space_init gauge\njvm_memory_pools_PS_Eden_Space_init 4.22576128E8\n# TYPE jvm_memory_pools_PS_Eden_Space_max gauge\njvm_memory_pools_PS_Eden_Space_max 1.110966272E9\n# TYPE jvm_memory_pools_PS_Eden_Space_usage gauge\njvm_memory_pools_PS_Eden_Space_usage 0.17698545577448457\n# TYPE jvm_memory_pools_PS_Eden_Space_used gauge\njvm_memory_pools_PS_Eden_Space_used 1.96624872E8\n# TYPE jvm_memory_pools_PS_Eden_Space_used_after_gc gauge\njvm_memory_pools_PS_Eden_Space_used_after_gc 0.0\n# TYPE jvm_memory_pools_PS_Old_Gen_committed gauge\njvm_memory_pools_PS_Old_Gen_committed 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_init gauge\njvm_memory_pools_PS_Old_Gen_init 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_max gauge\njvm_memory_pools_PS_Old_Gen_max 2.253914112E9\n# TYPE jvm_memory_pools_PS_Old_Gen_usage gauge\njvm_memory_pools_PS_Old_Gen_usage 0.014950035505168354\n# TYPE jvm_memory_pools_PS_Old_Gen_used gauge\njvm_memory_pools_PS_Old_Gen_used 3.3696096E7\n# TYPE jvm_memory_pools_PS_Old_Gen_used_after_gc gauge\njvm_memory_pools_PS_Old_Gen_used_after_gc 2.3791152E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_committed gauge\njvm_memory_pools_PS_Survivor_Space_committed 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_init gauge\njvm_memory_pools_PS_Survivor_Space_init 7.0254592E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_max gauge\njvm_memory_pools_PS_Survivor_Space_max 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_usage gauge\njvm_memory_pools_PS_Survivor_Space_usage 0.59375\n# TYPE jvm_memory_pools_PS_Survivor_Space_used gauge\njvm_memory_pools_PS_Survivor_Space_used 622592.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_used_after_gc gauge\njvm_memory_pools_PS_Survivor_Space_used_after_gc 622592.0\n# TYPE jvm_memory_total_committed gauge\njvm_memory_total_committed 1.644478464E9\n# TYPE jvm_memory_total_init gauge\njvm_memory_total_init 1.697972224E9\n# TYPE jvm_memory_total_max gauge\njvm_memory_total_max 3.999268864E9\n# TYPE jvm_memory_total_used gauge\njvm_memory_total_used 3.82665128E8\n# TYPE jvm_thread_blocked_count gauge\njvm_thread_blocked_count 0.0\n# TYPE jvm_thread_count gauge\njvm_thread_count 82.0\n# TYPE jvm_thread_daemon_count gauge\njvm_thread_daemon_count 11.0\n# TYPE jvm_thread_deadlock_count gauge\njvm_thread_deadlock_count 0.0\n# TYPE jvm_thread_new_count gauge\njvm_thread_new_count 0.0\n# TYPE jvm_thread_runnable_count gauge\njvm_thread_runnable_count 25.0\n# TYPE jvm_thread_terminated_count gauge\njvm_thread_terminated_count 0.0\n# TYPE jvm_thread_timed_waiting_count gauge\njvm_thread_timed_waiting_count 10.0\n# TYPE jvm_thread_waiting_count gauge\njvm_thread_waiting_count 47.0\n```"},{"name":"netty-server.md","id":"/topics/netty-server.md","url":"/topics/netty-server.html","title":"Alternative HTTP server","content":"# Alternative HTTP server\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nwith the change of licence in Akka, we are experimenting around using Netty as http server for otoroshi (and getting rid of akka http)\n\nin `v1.5.14` we are introducing a new alternative http server base on [`reactor-netty`](https://projectreactor.io/docs/netty/release/reference/index.html). It also include a preview of an HTTP3 server using [netty-incubator-codec-quic](https://github.com/netty/netty-incubator-codec-quic) and [netty-incubator-codec-http3](https://github.com/netty/netty-incubator-codec-http3)\n\n## The specs\n\nthis new server can start during otoroshi boot sequence and accept HTTP/1.1 (with and without TLS), H2C and H2 (with and without TLS) connections and supporting both standard HTTP calls and websockets calls.\n\n## Enable the server\n\nto enable the server, just turn on the following flag\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\n```\n\nnow you should see something like the following in the logs\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Server options\n\nyou can also setup the host and ports of the server using\n\n```conf\notoroshi.next.experimental.netty-server.host = \"0.0.0.0\"\notoroshi.next.experimental.netty-server.http-port = 10049\notoroshi.next.experimental.netty-server.https-port = 10048\n```\n\nyou can also enable access logs using\n\n```conf\notoroshi.next.experimental.netty-server.accesslog = true\n```\n\nand enable wiretaping using \n\n```conf\notoroshi.next.experimental.netty-server.wiretap = true\n```\n\nyou can also custom number of worker thread using\n\n```conf\notoroshi.next.experimental.netty-server.thread = 0 # system automatically assign the right number of threads\n```\n\n## HTTP2\n\nyou can enable or disable HTTP2 with\n\n```conf\notoroshi.next.experimental.netty-server.http2.enabled = true\notoroshi.next.experimental.netty-server.http2.h2c = true\n```\n\n## HTTP3\n\nyou can enable or disable HTTP3 (preview ;) ) with\n\n```conf\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048 # yep can the the same as https because its on the UDP stack\n```\n\nthe result will be something like\n\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Native transport\n\nIt is possible to enable native transport for the server\n\n```conf\notoroshi.next.experimental.netty-server.native.enabled = true\notoroshi.next.experimental.netty-server.native.driver = \"Auto\"\n```\n\npossible values for `otoroshi.next.experimental.netty-server.native.driver` are \n\n- `Auto`: the server try to find the best native option available\n- `Epoll`: the server uses Epoll native transport for Linux environments\n- `KQueue`: the server uses KQueue native transport for MacOS environments\n- `IOUring`: the server uses IOUring native transport for Linux environments that supports it (experimental, using [netty-incubator-transport-io_uring](https://github.com/netty/netty-incubator-transport-io_uring))\n\nthe result will be something like when starting on a Mac\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - using KQueue native transport\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Env. variables\n\nyou can configure the server using the following env. variables\n\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER`\n\n"},{"name":"opentelemetry.md","id":"/topics/opentelemetry.md","url":"/topics/opentelemetry.html","title":"OpenTelemetry support","content":"# OpenTelemetry support\n\nOpenTelemetry is an open-source project focused on providing a set of APIs, libraries, agents, and instrumentation to \nenable observability in modern software applications. It helps developers and software teams collect, process, \nand export telemetry data, which includes metrics, traces, and logs, from their applications and infrastructure. \nThe project aims to provide a standardized approach to instrumenting applications for distributed tracing, metrics, and logging.\n\nHere's a breakdown of the key components of OpenTelemetry:\n\n- **Tracing**: Distributed tracing is a method used to monitor and understand the flow of requests across different services \nin a distributed system. OpenTelemetry allows developers to add instrumentation to their code to trace requests as they \nflow through various services, providing insights into performance bottlenecks and dependencies between components.\n- **Metrics**: Metrics are quantitative measurements that provide information about the behavior and performance of \nan application. OpenTelemetry enables developers to collect metrics from their applications, such as CPU usage, memory \nconsumption, and custom application-specific metrics, to gain visibility into the application's health and performance.\n- **Logging**: OpenTelemetry also supports capturing and exporting logs, which are textual records of events and messages \nthat occur during the execution of an application. Logs are essential for debugging and monitoring purposes, and \nOpenTelemetry allows developers to integrate logging with other telemetry data, making it easier to correlate events.\n\nOpenTelemetry is designed to be language-agnostic and vendor-agnostic, supporting multiple programming languages and \nvarious telemetry backends. This flexibility makes it easier for developers to adopt the OpenTelemetry standard \nregardless of their technology stack.\n\nThe goal of OpenTelemetry is to promote a consistent way of collecting telemetry data across different applications \nand environments, making it easier for developers to adopt observability best practices. By leveraging OpenTelemetry, \nsoftware teams can gain deeper insights into the behavior of their systems and improve performance, troubleshoot \nissues, and enhance the overall reliability of their applications.\n\nNow, OpenTelemetry is officialy supported in Otoroshi and can be used in different parts of your instance. You can use \nit to collect otoroshi server logs and otoroshi server metrics through config. file. Then you have access to 2 new data \nexporter that can export otoroshi events to OpenTelemetry log collector and send custom metrics to an OpenTelemetry metrics collector.\n\n## server logs\n\notoroshi server logs can be sent to an OpenTelemetry log collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-logs.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the logs push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-logs {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## server metrics\n\notoroshi server metrics can be sent to an OpenTelemetry metrics collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-metrics.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the metrics push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-metrics {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## logs data expoter\n\nA new kind of data exporter is now available to send otoroshi events serialized as text to an OpenTelemetry log collector. \nFirst create a new data exporter and select the type `otlp-logs`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\n@@@ div { .centered-img }\n\n@@@\n\n## metrics data exporter\n\nA new kind of data exporter is now available to send custom metrics derived from otoroshi events to an OpenTelemetry metrics collector. \nFirst create a new data exporter and select the type `otlp-metrics`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\nThen you will be able to add new metrics on this data exporter with a name, the type of metric (counter, timer, histogram), the value and the kind of event it's based on.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n\n\n"},{"name":"otoroshi-protocol.md","id":"/topics/otoroshi-protocol.md","url":"/topics/otoroshi-protocol.html","title":"The Otoroshi communication protocol","content":"# The Otoroshi communication protocol\n\nThe exchange protocol secure the communication with an app. When it's enabled, Otoroshi will send for each request a value in pre-selected token header, and will check the same header in the return request. On routes, you will have to use the `Otoroshi challenge token` plugin to enable it.\n\n### V1 challenge\n\nIf you enable secure communication for a given service with `V1 - simple values exchange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header and return it in a header named `Otoroshi-State-Resp`. \n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V1 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### V2 challenge\n\nIf you enable secure communication for a given service with `V2 - signed JWT token exhange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header value containing a JWT token, verify it's content signature then extract a claim named `state` and return a new JWT token in a header named `Otoroshi-State-Resp` with the `state` value in a claim named `state-resp`. By default, the signature algorithm is HMAC+SHA512 but can you can choose your own. The sent and returned JWT tokens have short TTL to avoid being replayed. You must be validate the tokens TTL. The audience of the response token must be `Otoroshi` and you have to specify `iat`, `nbf` and `exp`.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V2 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### Info. token\n\nOtoroshi is also sending a JWT token in a header named `Otoroshi-Claim` that the target app can validate too. On routes, you will have to use the `Otoroshi info. token` plugin to enable it.\n\nThe `Otoroshi-Claim` is a JWT token containing some informations about the service that is called and the client if available. You can choose between a legacy version of the token and a new one that is more clear and structured.\n\nBy default, the otoroshi jwt token is signed with the `otoroshi.claim.sharedKey` config property (or using the `$CLAIM_SHAREDKEY` env. variable) and uses the `HMAC512` signing algorythm. But it is possible to customize how the token is signed from the service descriptor page in the `Otoroshi exchange protocol` section. \n\n@@@ div { .centered-img }\n\n@@@\n\nusing another signing algo.\n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can choose the signing algorithm and the secret/keys used. You can use syntax like `${env.MY_ENV_VAR}` or `${config.my.config.path}` to provide secret/keys values. \n\nFor example, for a service named `my-service` with a signing key `secret` with `HMAC512` signing algorythm, the basic JWT token that will be sent should look like the following\n\n```\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiItLSIsImF1ZCI6Im15LXNlcnZpY2UiLCJpc3MiOiJPdG9yb3NoaSIsImV4cCI6MTUyMTQ0OTkwNiwiaWF0IjoxNTIxNDQ5ODc2LCJqdGkiOiI3MTAyNWNjMTktMmFjNy00Yjk3LTljYzctMWM0ODEzYmM1OTI0In0.mRcfuFVFPLUV1FWHyL6rLHIJIu0KEpBkKQCk5xh-_cBt9cb6uD6enynDU0H1X2VpW5-bFxWCy4U4V78CbAQv4g\n```\n\nif you decode it, the payload will look something like\n\n```json\n{\n \"sub\": \"apikey_client_id\",\n \"aud\": \"my-service\",\n \"iss\": \"Otoroshi\",\n \"exp\": 1521449906,\n \"iat\": 1521449876,\n \"jti\": \"71025cc19-2ac7-4b97-9cc7-1c4813bc5924\"\n}\n```\n\nIf you want to validate the `Otoroshi-Claim` on the target app side to ensure that the input requests only comes from `Otoroshi`, you will have to write an HTTP filter to do the job. For instance, if you want to write a filter to make sure that requests only comes from Otoroshi, you can write something like the following (using playframework 2.6).\n\nScala\n: @@snip [filter.scala](../snippets/filter.scala)\n\nJava\n: @@snip [filter.java](../snippets/filter.java)\n"},{"name":"pki.md","id":"/topics/pki.md","url":"/topics/pki.html","title":"Otoroshi's PKI","content":"# Otoroshi's PKI\n\nWith Otoroshi, you can add your own certificates, your own CA and even create self signed certificates or certificates from CAs. You can enable auto renewal of thoses self signed certificates or certificates generated. Certificates have to be created with the certificate chain and the private key in PEM format.\n\nAn Otoroshi instance always starts with 5 auto-generated certificates. \n\nThe highest certificate is the **Otoroshi Default Root CA Certificate**. This certificate is used by Otoroshi to sign the intermediate CA.\n\n**Otoroshi Default Intermediate CA Certificate**: first intermediate CA that must be used to issue new certificates in Otoroshi. Creating certificates directly from the CA root certificate increases the risk of root certificate compromise, and if the CA root certificate is compromised, the entire trust infrastructure built by the SSL provider will fail\n\nThis intermediate CA signed three certificates :\n\n* **Otoroshi Default Client certificate**: \n* **Otoroshi Default Jwt Signing Keypair**: default keypair (composed of a public and private key), exposed on `https://xxxxxx/.well-known/jwks.json`, that can be used to sign and verify JWT verifier\n* **Otoroshi Default Wildcard Certificate**: this certificate has `*.oto.tools` as common name. It can be very useful to the development phase\n\n## The PKI API\n\nThe Otoroshi's PKI can be managed using the admin api of otoroshi (by default admin api is exposed on https://otoroshi-api.xxxxx)\n\nLink to the complete swagger section about PKI : https://maif.github.io/otoroshi/swagger-ui/index.html#/pki\n\n* `POST` [/api/pki/certs/_letencrypt](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genLetsEncryptCert): generates a certificate using Let's Encrypt or any ACME compatible system\n* `POST` [/api/pki/certs/_p12](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.importCertFromP12): import a .p12 file as client certificates\n* `POST` [/api/pki/certs/_valid](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateIsValid): check if a certificate is valid (based on its own data)\n* `POST` [/api/pki/certs/_data](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateData): extract data from a certificate\n* `POST` [/api/pki/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCert): generates a self signed certificates\n* `POST` [/api/pki/csrs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCsr) : generates a CSR\n* `POST` [/api/pki/keys](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genKeyPair) : generates a keypair\n* `POST` [/api/pki/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCA) : generates a self signed CA\n* `POST` [/api/pki/cas/:ca/certs/_sign](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.signCert): sign a certificate based on CSR\n* `POST` [/api/pki/cas/:ca/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCert): generates a certificate\n* `POST` [/api/pki/cas/:ca/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSubCA) : generates a sub-CA\n\n## The PKI UI\n\nAll generated certificates are listed in the `https://xxxxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe PKI UI are composed of these following actions:\n\n* **Add item**: redirects the user on the certificate creation page. It’s useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* **Let's Encrypt certificate**: asks a certificate matching a given host to Let’s encrypt\n* **Create certificate**: issues a certificate with an existing Otoroshi certificate as CA. You can create a client certificate, a server certificate or a keypair certiciate that will be used to verify and sign JWT tokens.\n* **Import .p12 file**: loads a p12 file as certificate\n\nUnder these buttons, you have the list of current certificates, imported or generated, revoked or not. For each certificate, you will find: \n\n* a **name** \n* a **description** \n* the **subject** \n* the **type** of certificate (CA / client / keypair / certificate)\n* the **revoked reason** (empty if not) \n* the **creation date** following by its **expiration date**.\n\n## Exposed public keys\n\nThe Otoroshi certificate can be turned and used as keypair (simple action that can be executed by editing a certificate or during its creation, or using the admin api). A Otoroski keypair can be used to sign and verify JWT tokens with asymetric signature. Once a jwt token is signed with a keypair, it can be necessary to provide a way to the services to verify the tokens received by Otoroshi. This usage is cover by Otoroshi by the flag `Public key exposed`, available on each certificate.\n\nOtoroshi exposes each keypair with the flag enabled, on the following routes:\n\n* `https://xxxxxxxxx.xxxxxxx.xx/.well-known/otoroshi/security/jwks.json`\n* `https://otoroshi-api.xxxxxxx.xx/.well-known/jwks.json`\n\nOn these routes, you will find the list of public keys exposed using [the JWK standard](https://datatracker.ietf.org/doc/html/rfc7517)\n\n\n## OCSP Responder\n\nOtoroshi is able to revocate a certificate, directly from the UI, and to add a revocation status to specifiy the reason. The revocation reason can be :\n\n* `VALID`: The certificate is not revoked\n* `UNSPECIFIED`: Can be used to revoke certificates for reasons other than the specific codes.\n* `KEY_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `CA_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `AFFILIATION_CHANGED`: The subject's name or other information in the certificate has been modified but there is no cause to suspect that the private key has been compromised.\n* `SUPERSEDED`: The certificate has been superseded but there is no cause to suspect that the private key has been compromised\n* `CESSATION_OF_OPERATION`: The certificate is no longer needed for the purpose for which it was issued but there is no cause to suspect that the private key has been compromised\n* `CERTIFICATE_HOLD`: The certificate is temporarily revoked but there is no cause to suspect that the private kye has been compromised\n* `REMOVE_FROM_CRL`: The certificate has been unrevoked\n* `PRIVILEGE_WITH_DRAWN`: The certificate was revoked because a privilege contained within that certificate has been withdrawn\n* `AA_COMPROMISE`: It is known or suspected that aspects of the AA validated in the attribute certificate, have been compromised\n\nOtoroshi supports the Online Certificate Status Protocol for obtaining the revocation status of its certificates. The OCSP endpoint is also add to any generated certificate. This endpoint is available at `https://otoroshi-api.xxxxxx/.well-known/otoroshi/security/ocsp`\n\n## A.I.A : Authority Information Access\n\nOtoroshi provides a way to add the A.I.A in the certificate. This certificate extension contains :\n\n* Information about how to get the issuer of this certificate (CA issuer access method)\n* Address of the OCSP responder from where revocation of this certificate can be checked (OCSP access method)\n\n`https://xxxxxxxxxx/.well-known/otoroshi/security/certificates/:cert-id`"},{"name":"relay-routing.md","id":"/topics/relay-routing.md","url":"/topics/relay-routing.html","title":"Relay Routing","content":"# Relay Routing\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nRelay routing is the capability to forward traffic between otoroshi leader nodes based on network location of the target. Let say we have an otoroshi cluster split accross 3 network zones. Each zone has \n\n- one or more datastore instances\n- one or more otoroshi leader instances\n- one or more otoroshi worker instances\n\nthe datastores are replicated accross network zones in an active-active fashion. Each network zone also have applications, apis, etc deployed. Sometimes the same application is deployed in multiple zones, sometimes not. \n\nit can quickly become a nightmare when you want to access an application deployed in one network zone from another network zone. You'll have to publicly expose this application to be able to access it from the other zone. This pattern is fine, but sometimes it's not enough. With `relay routing`, you will be able to flag your routes as being deployed in one zone or another, and let otoroshi handle all the heavy lifting to route the traffic to the right network zone for you.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through relay nodes\n@@@\n\n## Otoroshi instance setup\n\nfirst of all, for every otoroshi instance deployed, you have to flag where the instance is deployed and, for leaders, how this instance can be contacted from other zones (this is a **MAJOR** requirement, without that, you won't be able to make relay routing work). Also, you'll have to enable the @ref:[new proxy engine](./engine.md).\n\nIn the otoroshi configuration file, for each instance, enable relay routing and configure where the instance is located and how the leader can be contacted\n\n```conf\notoroshi {\n ...\n cluster {\n mode = \"leader\" # or \"worker\" dependending on the instance kind\n ...\n relay {\n enabled = true # enable relay routing\n leaderOnly = true # use leaders as the only kind of relay node\n location { # you can use all those parameters at the same time. There is no actual network concepts bound here, just some kind of tagging system, so you can use it as you wish\n provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER}\n zone = \"zone-1\"\n region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION}\n datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER}\n rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK}\n }\n exposition {\n urls = [\"https://otoroshi-api-zone-1.my.domain:443\"]\n hostname = \"otoroshi-api-zone-1.my.domain\"\n clientId = \"apkid_relay-routing-apikey\"\n }\n }\n }\n}\n```\n\nalso, to make your leaders exposed by zone, do not hesitate to add domain names to the `otoroshi-admin-api` service and setup your DNS to bind those domains to the right place\n\n@@@ div { .centered-img }\n\n@@@\n\n## Route setup for an application deployed in only one zone\n\nNow, for any route/service deployed in only one zone, you will be able to flag it using its metadata as being deployed in one zone or another. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1`\n\n## Route setup for an application deployed in multiple zones at the same time\n\nNow, for any route/service deployed in multiple zones zones at the same time, you will be able to flag it using its metadata as being deployed in some zones. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1, zone-2` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1` or `zone-2`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1` or `zone-2` and load balance between them.\n\nalso, you will have to setup your targets to avoid trying to contact targets that are not actually in the current zone. To do that, you'll have to set the target predicate to `NetworkLocationMatch` and fill the possible locations according to the actual location of your target\n\n@@@ div { .centered-img }\n\n@@@\n\n## Demo\n\nyou can find a demo of this setup [here](https://github.com/MAIF/otoroshi/tree/master/demos/relay). This is a `docker-compose` setup with multiple network to simulate network zones. You also have an otoroshi export to understand how to setup your routes/services\n"},{"name":"secrets.md","id":"/topics/secrets.md","url":"/topics/secrets.html","title":"Secrets management","content":"# Secrets management\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSecrets are generally confidential values that should not appear in plain text in the application. There are several products that help you store, retrieve, and rotate these secrets securely. Otoroshi offers a mechanism to set up references to these secrets in its entities to benefits from the perks of your existing secrets management infrastructure. This feature only work with the @ref:[new proxy engine](./engine.md).\n\nA secret can be anything you want like an apikey secret, a certificate private key or password, a jwt verifier signing key, a password to a proxy, a value for a header, etc.\n\n## Enable secrets management in otoroshi\n\nBy default secrets management is disbaled. You can enable it by setting `otoroshi.vaults.enabled` or `${OTOROSHI_VAULTS_ENABLED}` to `true`.\n\n## Global configuration\n\nSecrets management can be only configured using otoroshi static configuration file (also using jvm args mechanism). \nThe configuration is located at `otoroshi.vaults` where you can find the global configuration of the secrets management system and the configurations for each enabled secrets management backends. Basically it looks like\n\n```conf\nvaults {\n enabled = false\n enabled = ${?OTOROSHI_VAULTS_ENABLED}\n secrets-ttl = 300000 # 5 minutes\n secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL}\n cached-secrets = 10000\n cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS}\n read-timeout = 10000 # 10 seconds\n read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT}\n # if enabled, only leader nodes fetches the secrets.\n # entities with secret values filled are then sent to workers when they poll the cluster state.\n # only works if `otoroshi.cluster.autoUpdateState=true`\n leader-fetch-only = false\n leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY}\n env {\n type = \"env\"\n prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX}\n }\n}\n```\n\nyou can see here the global configuration and a default backend configured that can retrieve secrets from environment variables. \n\nThe configuration keys can be used for \n\n- `secrets-ttl`: the amount of milliseconds before the secret value is read again from backend\n- `cached-secrets`: the number of secrets that will be cached on an otoroshi instance\n- `read-timeout`: the timeout (in milliseconds) to read a secret from a backend\n\n## Entities with secrets management\n\nthe entities that support secrets management are the following \n\n- `routes`\n- `services`\n- `service_descriptors`\n- `apikeys`\n- `certificates`\n- `jwt_verifiers`\n- `authentication_modules`\n- `targets`\n- `backends`\n- `tcp_services`\n- `data_exporters`\n\n## Define a reference to a secret\n\nin the previously listed entities, you can define, almost everywhere, references to a secret using the following syntax:\n\n`${vault://name_of_the_vault/secret/of/the/path}`\n\nlet say I define a new apikey with the following value as secret `${vault://my_env/apikey_secret}` with the following secrets management configuration\n\n```conf\nvaults {\n enabled = true\n secrets-ttl = 300000\n cached-secrets = 10000\n read-ttl = 10000\n my_env {\n type = \"env\"\n }\n}\n```\n\nif the machine running otoroshi has an environment variable named `APIKEY_SECRET` with the value `verysecret`, then you will be able to can an api with the defined apikey `client_id` and a `client_secret` value of `verysecret`\n\n```sh\ncurl 'http://my-awesome-api.oto.tools:8080/api/stuff' -u awesome_apikey:verysecret\n```\n\n## Possible backends\n\nOtoroshi comes with the support of several secrets management backends.\n\n### Environment variables\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"env\"\n prefix = \"the_prefix_added_to_the_name_of_the_env_variable\"\n }\n}\n```\n\n### Local\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"local\"\n root = \"the_root_path/in_otoroshi/environment\"\n }\n}\n```\n\nvalue of this vault can be configured in the danger zone > Global metadata > Otoroshi environment.\n\n### Infisical\n\na backend for the awesome open source project [Infisical](https://infisical.com/). It support both E2EE and non E2EE secrets.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"infisical\"\n baseUrl = \"https://app.infisical.com\" # optional, the base url of your infisical server, fallbacks to https://app.infisical.com\n serviceToken = \"st.xxxx.yyyy.zzzz\" # the service token for your projet\n e2ee = true # are you secrets end to end encrypted\n defaultSecretType = \"shared\" # optional, fallbacks to shared\n defaultWorkspaceId = \"xxxxxx\" # optional, value can be passed in the secret address\n defaultEnvironment = \"dev\" # optional, value can be passed in the secret address\n }\n}\n```\n\nyou should define your references like `${vault://infisical_vault/my_secret_path?workspaceId=xxx&environment=dev&type=shared}`. `workspaceId`, `environment` and `type` are optional if filled in global config. \n\nYou can also pass a `json_pointer=/foo/bar` to handle the value like a json document a select a value inside it.\n\n### Hashicorp Vault\n\na backend for [Hashicorp Vault](https://www.vaultproject.io/). Right now we only support KV engines.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"hashicorp-vault\"\n url = \"http://127.0.0.1:8200\"\n mount = \"kv\" # the name of the secret store in vault\n kv = \"v2\" # the version of the kv store (v1 or v2)\n token = \"root\" # the token that can access to your secrets\n }\n}\n```\n\nyou should define your references like `${vault://hashicorp_vault/secret/path/key_name}`.\n\n\n### Azure Key Vault\n\na backend for [Azure Key Vault](https://azure.microsoft.com/en-en/services/key-vault/). Right now we only support secrets and not keys and certificates.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"azure\"\n url = \"https://keyvaultname.vault.azure.net\"\n api-version = \"7.2\" # the api version of the vault\n tenant = \"xxxx-xxx-xxx\" # your azure tenant id, optional\n client_id = \"xxxxx\" # your azure client_id\n client_secret = \"xxxxx\" # your azure client_secret\n # token = \"xxx\" possible if you have a long lived existing token. will take over tenant / client_id / client_secret\n }\n}\n```\n\nyou should define your references like `${vault://azure_vault/secret_name/secret_version}`. `secret_version` is mandatory\n\nIf you want to use certificates and keys objects from the azure key vault, you will have to specify an option in the reference named `azure_secret_kind` with possible value `certificate`, `privkey`, `pubkey` like the following :\n\n```\n${vault://azure_vault/myprivatekey/secret_version?azure_secret_kind=privkey}\n```\n\n### AWS Secrets Manager\n\na backend for [AWS Secrets Manager](https://aws.amazon.com/en/secrets-manager/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"aws\"\n access-key = \"key\"\n access-key-secret = \"secret\"\n region = \"eu-west-3\" # the aws region of your secrets management\n }\n}\n```\n\nyou should define your references like `${vault://aws_vault/secret_name/secret_version}`. `secret_version` is optional\n\n### Google Cloud Secrets Manager\n\na backend for [Google Cloud Secrets Manager](https://cloud.google.com/secret-manager)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"gcloud\"\n url = \"https://secretmanager.googleapis.com\"\n apikey = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://gcloud_vault/projects/foo/secrets/bar/versions/the_version}`. `the_version` can be `latest`\n\n### AlibabaCloud Cloud Secrets Manager\n\na backend for [AlibabaCloud Secrets Manager](https://www.alibabacloud.com/help/en/doc-detail/152001.html)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"alibaba-cloud\"\n url = \"https://kms.eu-central-1.aliyuncs.com\"\n access-key-id = \"access-key\"\n access-key-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://alibaba_vault/secret_name}`\n\n\n### Kubernetes Secrets\n\na backend for [Kubernetes secrets](https://kubernetes.io/en/docs/concepts/configuration/secret/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"kubernetes\"\n # see the configuration of the kubernetes plugin, \n # by default if the pod if well configured, \n # you don't have to setup anything\n }\n}\n```\n\nyou should define your references like `${vault://k8s_vault/namespace/secret_name/key_name}`. `key_name` is optional. if present, otoroshi will try to lookup `key_name` in the secrets `stringData`, if not defined the secrets `data` will be base64 decoded and used.\n\n\n### Izanami config.\n\na backend for [Izanami config.](https://maif.github.io/izanami/manual/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"izanami\"\n url = \"http://127.0.0.1:8200\"\n client-id = \"client\"\n client-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://izanami_vault/the:secret:id/key_name}`. `key_name` is optional if the secret value is not a json object\n\n### Spring Cloud Config\n\na backend for [Spring Cloud Config.](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"spring-cloud\"\n url = \"http://127.0.0.1:8000\"\n root = \"myapp/prod\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://spring_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n\n### Http backend\n\na backend for that uses the result of an http endpoint\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"http\"\n url = \"http://127.0.0.1:8000/endpoint/for/config\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://http_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n"},{"name":"sessions-mgmt.md","id":"/topics/sessions-mgmt.md","url":"/topics/sessions-mgmt.html","title":"Sessions management","content":"# Sessions management\n\n## Admins\n\nAll logged users to an Otoroshi instance are administrators. An user session is created for each sucessfull connection to the UI. \n\nThese sessions are listed in the `Admin users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/admin`).\n\nAn admin user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Rights`: list of rules to authorize the connected user on each tenant and teams.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n\nIn the `Admin users sessions` page, you have two more actions:\n\n* `Discard all sessions`: kills all current sessions (including the session of the owner of this action)\n* `Discard old sessions`: kill all outdated sessions\n\n## Private apps\n\nAll logged users to a protected application has an private user session.\n\nThese sessions are listed in the `Private apps users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/private`).\n\nAn private user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Meta.`: list of metadatas added by the authentication module.\n* `Tokens`: list of tokens received from the identity provider used. In the case of a memory authentication, this part will keep empty.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n"},{"name":"tls.md","id":"/topics/tls.md","url":"/topics/tls.html","title":"TLS","content":"# TLS\n\nas you might have understand, otoroshi can store TLS certificates and use them dynamically. It means that once a certificate is imported or created in otoroshi, you can immediately use it to serve http request over TLS, to call https backends that requires mTLS or that do not have certicates signed by a globally knowned authority.\n\n## TLS termination\n\nany certficate added to otoroshi with a valid `CN` and `SANs` can be used in the following seconds to serve https requests. If you do not provide a private key with a certificate chain, the certificate will only be trusted like a CA. If you want to perform mTLS calls on you otoroshi instance, do not forget to enabled it (it is disabled by default for performance reasons as the TLS handshake is bigger with mTLS enabled)\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\n### TLS termination configuration\n\nYou can configure TLS termination statically using config. file or env. variables. Everything is available at `otoroshi.tls`\n\n```conf\notoroshi {\n tls {\n # the cipher suites used by otoroshi TLS termination\n cipherSuitesJDK11 = [\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuitesJDK8 = [\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_RSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuites = []\n # the protocols used by otoroshi TLS termination\n protocolsJDK11 = [\"TLSv1.3\", \"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"]\n protocolsJDK8 = [\"SSLv2Hello\", \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n protocols = []\n # the JDK cacert access\n cacert {\n path = \"$JAVA_HOME/lib/security/cacerts\"\n password = \"changeit\"\n }\n # the mtls mode\n fromOutside {\n clientAuth = \"None\"\n clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH}\n }\n # the default trust mode\n trust {\n all = false\n all = ${?OTOROSHI_SSL_TRUST_ALL}\n }\n # some initial cacert access, useful to include non standard CA when starting (file paths)\n initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT}\n initialCacert = ${?INITIAL_CACERT}\n initialCert = ${?CLUSTER_WORKER_INITIAL_CERT}\n initialCert = ${?INITIAL_CERT}\n initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY}\n initialCertKey = ${?INITIAL_CERT_KEY}\n # initialCerts = [] \n }\n}\n```\n\n\n### TLS termination settings\n\nIt is possible to adjust the behavior of the TLS termination from the `danger zone` at the `Tls Settings` section. Here you can either define that a non-matching SNI call will use a random TLS certtificate to reply or will use a default domain (the TLS certificate associated to this domain) to reply. Here you can also choose if you want to trust all the CAs trusted by your JDK when performing TLS calls `Trust JDK CAs (client)` or when receiving mTLS calls `Trust JDK CAs (server)`. If you disable the later, it is possible to select the list of CAs presented to the client during mTLS handshake.\n\n### Certificates auto generation\n\nit is also possible to generate non-existing certificate on the fly without losing the request. If you are interested by this feature, you can enable it in the `danger zone` at the `Auto Generate Certificates` section. Here you'll have to enable it and select the CA that will generate the certificate. Of course, the client will have to trust the selected CA. You can also add filters to choose which domain are allowed to generate certificates or not. The `Reply Nicely` flag is used to reply a nice error message (ie. human readable) telling that it's not possible to have an auto certficate for the current domain. \n\n## Backends TLS and mTLS calls\n\nFor any call to a backend, it is possible to customize the TLS behavior \n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can define your level of trust (trust all, loose verification) or even select on or more CAs you will trust for the following backend calls. You can also select the client certificate that will be used for the following backend calls\n\n## Keypair for signing and verification\n\nIt is also possible to use the keypair contained in a certificate to sign and verificate JWT token signature. You can mark an existing certificate in otoroshi as a keypair using the `keypair` on the certificate page.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"tunnels.md","id":"/topics/tunnels.md","url":"/topics/tunnels.html","title":"Otoroshi tunnels","content":"# Otoroshi tunnels\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSometimes, exposing apis that lives in our private network can be a nightmare, especially from a networking point of view. \nWith otoroshi tunnels, this is now trivial, as long as your internal otoroshi (that lives inside your private network) is able to contact an external otoroshi (exposed on the internet).\n\n@@@ warning { .margin-top-20 }\nYou have to enable cluster mode (Leader or Worker) to make this feature work. As this feature is experimental, we only support simple http request right now. Server Sent Event and Websocket request are not supported at the moment.\n@@@\n\n## How Otoroshi tunnels works\n\nthe main idea behind otoroshi tunnels is that the connection between your private network et the public network is initiated by the private network side. You don't have to expose a part of your private network, create a DMZ or whatever, you just have to authorize your private network otoroshi instance to contact your public network otoroshi instance.\n\n@@@ div { .centered-img }\n\n@@@\n\nonce the persistent tunnel has been created, you can create routes on the public otoroshi instance that uses the otoroshi `Remote tunnel calls` to target your remote routes through the designated tunnel instance \n\n\n@@@ div { .centered-img }\n\n@@@\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through otoroshi tunnels\n@@@\n\n## Otoroshi tunnel example\n\nfirst you have to enable the tunnels feature in your otoroshi configuration (on both public and private instances)\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n enabled = ${?OTOROSHI_TUNNELS_ENABLED}\n ...\n }\n}\n```\n\nthen you can setup a tunnel instance on your private instance to contact your public instance\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n ...\n public-apis {\n id = \"public-apis\"\n name = \"public apis tunnel\"\n url = \"https://otoroshi-api.company.com:443\"\n host = \"otoroshi-api.company.com\"\n clientId = \"xxx\"\n clientSecret = \"xxxxxx\"\n # ipAddress = \"127.0.0.1\" # optional: ip address of the public instance admin api\n # tls { # optional: TLS settings to access the public instance admin api\n # ... \n # }\n # export-routes = true # optional: send routes information to remote otoroshi instance to facilitate remote route exposition\n # export-routes-tag = \"tunnel-exposed\" # optional: only send routes information if the route has this tag\n }\n }\n}\n```\n\nNow when your private otoroshi instance will boot, a persistent tunnel will be made between private and public instance. \nNow let say you have a private api exposed on `api-a.company.local` on your private otoroshi instance and you want to expose it on your public otoroshi instance. \n\nFirst create a new route exposed on `api-a.company.com` that targets `https://api-a.company.local:443`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen add the `Remote tunnel calls` plugin to your route and set the tunnel id to `public-apis` to match the id you set in the otoroshi config file\n\n@@@ div { .centered-img }\n\n@@@\n\nadd all the plugin you need to secure this brand new public api and call it\n\n```sh\ncurl \"https://api-a.company.com/users\" | jq\n```\n\n## Easily expose your remote services\n\nyou can see all the connected tunnel instances on an otoroshi instance on the `Connected tunnels` (`Cog icon` / `Connected tunnels`). For each tunnel instance you will be able to check the tunnel health and also to easily expose all the routes available on the other end of the tunnel. Just clic on the `expose` button of the route you want to expose, and a new route will be created with the `Remote tunnel calls` plugin already setup.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"user-rights.md","id":"/topics/user-rights.md","url":"/topics/user-rights.html","title":"Otoroshi user rights","content":"# Otoroshi user rights\n\nIn Otoroshi, all users are considered **Administrators**. This choice is reinforced by the fact that Otoroshi is designed to be an administrator user interface and not an interface for users who simply want to view information. For this type of use, we encourage to use the admin API rather than giving access to the user interface.\n\nThe Otoroshi rights are split by a list of authorizations on **organizations** and **teams**. \n\nLet's taking an example where we want to authorize an administrator user on all organizations and teams.\n\nThe list of rights will be :\n\n```json\n[\n {\n \"tenant\": \"*:rw\", # (1)\n \"teams\": [\"*:rw\"] # (2)\n }\n]\n```\n\n* (1): this field, separated by a colon, indicates the name of the tenant and the associated rights. In our case, we set `*` to apply the rights to all tenants, and the `rw` to get the read and write access on them.\n* (2): the `teams` array field, represents the list of rights, applied by team. The behaviour is the same as the tenant field, we define the team or the wildcard, followed by the rights\n\nif you want to have an user that is administrator only for one organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that is administrator only for two organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that can only see 3 teams of one organization and one team in the other, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\n \"team-1:rw\",\n \"team-2:rw\",\n \"team-3:rw\",\n ]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\n \"team-4:rw\"\n ]\n }\n]\n```\n\nThe list of possible rights for an organization or a team is:\n\n* **r**: read access\n* **w**: write access\n* **not**: none access to the resource\n\nThe list of possible tenant and teams are your created tenants and teams, and the wildcard to define rights to all resources once.\n\nThe user rights is defined by the @ref:[authentication modules](../entities/auth-modules.md).\n"},{"name":"wasm-usage.md","id":"/topics/wasm-usage.md","url":"/topics/wasm-usage.html","title":"Otoroshi and WASM","content":"# Otoroshi and WASM\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. These plugins are:\n\n- `WasmRouteMatcher`: useful to define if a route can handle a request\n- `WasmPreRoute`: useful to check request and extract useful stuff for the other plugins\n- `WasmAccessValidator`: useful to control access to a route (jump to the next section to learn more about it)\n- `WasmRequestTransformer`: transform the content of an incoming request (body, headers, etc ...)\n- `WasmBackend`: execute a WASM file as Otoroshi target. Useful to implement user defined logic and function at the edge\n- `WasmResponseTransformer`: transform the content of the response produced by the target\n- `WasmSink`: create a sink plugin to handle unmatched requests\n- `WasmRequestHandler`: create a plugin that can handle the whole request lifecycle\n- `WasmJob`: create a job backed by a wasm function\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Available tutorials\n\nhere is the list of available tutorials about wasm in Otoroshi\n\n1. @ref:[Install a Wasmo](../how-to-s/wasmo-installation.md)\n2. @ref:[Use a WASM plugin](../how-to-s/wasm-usage.md)\n\n## Wasm plugins entities\n\nOtoroshi provides a dedicated entity for wasm plugins. Those entities makes it easy to declare a wasm plugin with specific configuration only once and use it in multiple places. \n\nYou can find wasm plugin entities at `/bo/dashboard/wasm-plugins`\n\nIn a wasm plugin entity, you can define the source of your wasm plugin. You can choose between\n\n- `base64`: a base64 encoded wasm script\n- `file`: the path to a wasm script file\n- `http`: the url to a wasm script file\n- `wasmo`: the name of a wasm script compiled by a Wasmo instance\n\nthen you can define the number of memory pages available for each plugin instanciation, the name of the function you want to invoke, the config. map of the VM and if you want to keep a wasm vm alive during the request lifecycle to be able to reuse it in different plugin steps\n\n@@@ div { .centered-img }\n\n@@@\n\n## Otoroshi plugins api\n\nthe following parts illustrates the apis for the different plugins. Otoroshi uses [Extism](https://extism.org/) to handle content sharing between the JVM and the wasm VM. All structures are sent to/from the plugins as json strings. \n\nfor instance, if we want to write a `WasmBackendCall` plugin using javascript, we could write something like\n\n```js\nfunction backend_call() {\n const input_str = Host.inputString(); // here we get the context passed by otoroshi as json string\n const backend_call_context = JSON.parse(input_str); // and parse it\n if (backend_call_context.path === '/hello') {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n message: `Hello ${ctx.request.query.name[0]}!` \n },\n status: 200,\n }));\n } else {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n error: \"not found\"\n },\n status: 404,\n }));\n }\n return 0; // we return 0 to tell otoroshi that everything went fine\n}\n```\n\nthe following examples are written in rust. the rust macros provided by extism makes the usage of `Host.inputString` and `Host.outputString` useless. Remember that it's still used under the hood and that the structures are passed as json strings.\n\ndo not forget to add the extism pdk library to your project to make it compile\n\nCargo.toml\n: @@snip [Cargo.toml](../snippets/wasmo/Cargo.toml) \n\ngo.mod\n: @@snip [go.mod](../snippets/wasmo/go.mod) \n\npackage.json\n: @@snip [package.json](../snippets/wasmo/package.json) \n\n### WasmRouteMatcher\n\nA route matcher is a plugin that can help the otoroshi router to select a route instance based on your own custom predicate. Basically it's a function that returns a boolean answer.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn matches_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteResponse {\n pub result: bool,\n}\n```\n\n### WasmPreRoute\n\nA pre-route plugin can be used to short-circuit a request or enrich it (maybe extracting your own kind of auth. token, etc) a the very beginning of the request handling process, just after the routing part, when a route has bee chosen by the otoroshi router.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn pre_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteResponse {\n pub error: bool,\n pub attrs: Option>,\n pub status: Option,\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmAccessValidator\n\nAn access validator plugin is typically used to verify if the request can continue or must be cancelled. For instance, the otoroshi apikey plugin is an access validator that check if the current apikey provided by the client is legit and authorized on the current route.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_access(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorContext {\n pub snowflake: Option,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorError {\n pub message: String,\n pub status: u32,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorResponse {\n pub result: bool,\n pub error: Option,\n}\n```\n\n### WasmRequestTransformer\n\nA request transformer plugin can be used to compose or transform the request that will be sent to the backend\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmRequestTransformerContext {\n pub snowflake: Option,\n pub raw_request: OtoroshiRequest,\n pub otoroshi_request: OtoroshiRequest,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n}\n```\n\n### WasmBackendCall\n\nA backend call plugin can be used to simulate a backend behavior in otoroshi. For instance the static backend of otoroshi return the content of a file\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn call_backend(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmBackendContext {\n pub snowflake: Option,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub raw_request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n pub request: OtoroshiRequest,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmBackendResponse {\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n pub status: u32,\n}\n```\n\n### WasmResponseTransformer\n\nA response transformer plugin can be used to compose or transform the response that will be sent back to the client\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_response(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmResponseTransformerContext {\n pub snowflake: Option,\n pub raw_response: OtoroshiResponse,\n pub otoroshi_response: OtoroshiResponse,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub response_body_bytes: Option>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmTransformerResponse {\n pub headers: HashMap,\n pub cookies: Value,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmSink\n\nA sink is a kind of plugin that can be used to respond to any unmatched request before otoroshi sends back a 404 response\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn sink_matches(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn sink_handle(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkContext {\n pub snowflake: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub origin: String,\n pub status: u32,\n pub message: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkMatchesResponse {\n pub result: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkHandleResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmRequestHandler\n\nA request handler is a very special kind of plugin that can bypass the otoroshi proxy engine on specific domains and completely handles the request/response lifecycle on it's own.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerContext {\n pub request: RawRequest\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmJob\n\nA job is a plugin that can run periodically an do whatever you want. Typically, the kubernetes plugins of otoroshi are jobs that periodically sync stuff between otoroshi and kubernetes using the kube-api\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn job_run(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobContext {\n pub attrs: Value,\n pub global_config: Value,\n pub snowflake: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobResult {\n\n}\n```\n\n### Common types\n\n```rs\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Backend {\n pub id: String,\n pub hostname: String,\n pub port: u32,\n pub tls: bool,\n pub weight: u32,\n pub protocol: String,\n pub ip_address: Option,\n pub predicate: Value,\n pub tls_config: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Apikey {\n #[serde(alias = \"clientId\")]\n pub client_id: String,\n #[serde(alias = \"clientName\")]\n pub client_name: String,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct User {\n pub name: String,\n pub email: String,\n pub profile: Value,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RawRequest {\n pub id: u32,\n pub method: String,\n pub headers: HashMap,\n pub cookies: Value,\n pub tls: bool,\n pub uri: String,\n pub path: String,\n pub version: String,\n pub has_body: bool,\n pub remote: String,\n pub client_cert_chain: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Frontend {\n pub domains: Vec,\n pub strict_path: Option,\n pub exact: bool,\n pub headers: HashMap,\n pub query: HashMap,\n pub methods: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct HealthCheck {\n pub enabled: bool,\n pub url: String,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RouteBackend {\n pub targets: Vec,\n pub root: String,\n pub rewrite: bool,\n pub load_balancing: Value,\n pub client: Value,\n pub health_check: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Route {\n pub id: String,\n pub name: String,\n pub description: String,\n pub tags: Vec,\n pub metadata: HashMap,\n pub enabled: bool,\n pub debug_flow: bool,\n pub export_reporting: bool,\n pub capture: bool,\n pub groups: Vec,\n pub frontend: Frontend,\n pub backend: RouteBackend,\n pub backend_ref: Option,\n pub plugins: Value,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct OtoroshiResponse {\n pub status: u32,\n pub headers: HashMap,\n pub cookies: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct OtoroshiRequest {\n pub url: String,\n pub method: String,\n pub headers: HashMap,\n pub version: String,\n pub client_cert_chain: Value,\n pub backend: Option,\n pub cookies: Value,\n}\n```\n\n## Otoroshi interop. with host functions\n\notoroshi provides some host function in order make wasm interact with otoroshi internals. You can\n\n- access wasi resources\n- access http resources\n- access otoroshi internal state\n- access otoroshi internal configuration\n- access otoroshi static configuration\n- access plugin scoped in-memory key/value storage\n- access global in-memory key/value storage\n- access plugin scoped persistent key/value storage\n- access global persistent key/value storage\n\n### authorizations\n\nall the previously listed host functions are enabled with specific authorizations to avoid security issues with third party plugins. You can enable/disable the host function from the wasm plugin entity\n\n@@@ div { .centered-img }\n\n@@@\n\n\n### host functions abi\n\nyou'll find here the raw signatures for the otoroshi host functions. we are currently in the process of writing higher level functions to hide the complexity.\n\nevery time you the the following signature: `(context: u64, size: u64) -> u64` it means that otoroshi is expecting for a pointer to the call context (which is a json string) and it's size. The return is a pointer to the response (which is a json string).\n\nthe signature `(unused: u64) -> u64` means that there is no need for a params but as we technically need one (and hope to don't need one in the future), you have to pass something like `0` as parameter.\n\n```rust\nextern \"C\" {\n // log messages in otoroshi (log levels are 0 to 6 for trace, debug, info, warn, error, critical, max)\n fn proxy_log(logLevel: i32, message: u64, size: u64) -> i32;\n // trigger an otoroshi wasm event that can be exported through data exporters\n fn proxy_log_event(context: u64, size: u64) -> u64;\n // an http client\n fn proxy_http_call(context: u64, size: u64) -> u64;\n // access the current otoroshi state containing a snapshot of all otoroshi entities\n fn proxy_state(context: u64) -> u64;\n fn proxy_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi cluster configuration\n fn proxy_cluster_state(context: u64) -> u64;\n fn proxy_cluster_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi static configuration\n fn proxy_global_config(unused: u64) -> u64;\n // access the current otoroshi dynamic configuration\n fn proxy_config(unused: u64) -> u64;\n // access a persistent key/value store shared by every wasm plugins\n fn proxy_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_datastore_all_matching(context: u64, size: u64) -> u64;\n // access a persistent key/value store for the current plugin instance only\n fn proxy_plugin_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_all_matching(context: u64, size: u64) -> u64;\n // access an in memory key/value store for the current plugin instance only\n fn proxy_plugin_map_set(context: u64, size: u64) -> u64;\n fn proxy_plugin_map_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_map(unused: u64) -> u64;\n // access an in memory key/value store shared by every wasm plugins\n fn proxy_global_map_set(context: u64, size: u64) -> u64;\n fn proxy_global_map_get(context: u64, size: u64) -> u64;\n fn proxy_global_map(unused: u64) -> u64;\n}\n```\n\nright know, when using the Wasmo, a default idiomatic implementation is provided for `TinyGo` and `Rust`\n\nhost.rs\n: @@snip [host.rs](../snippets/wasmo/host.rs) \n\nhost.go\n: @@snip [host.go](../snippets/wasmo/host.go) \n"}] \ No newline at end of file diff --git a/docs/devmanual/deploy/kubernetes.html b/docs/devmanual/deploy/kubernetes.html index 904ef30801..77ac0ebee1 100644 --- a/docs/devmanual/deploy/kubernetes.html +++ b/docs/devmanual/deploy/kubernetes.html @@ -312,7 +312,7 @@

https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.

You can also create a kustomization.yaml file with a remote base

bases:
-- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.14.0-dev
+- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.15.0-dev
 

Then deploy it with kubectl apply -k ./overlays/myoverlay.

You can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster

@@ -365,7 +365,7 @@

Secure your web app in 2 calls with an authentication

Download the latest jar of Otoroshi

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

Once downloading, run Otoroshi.

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/export-events-to-elastic.html b/docs/devmanual/how-to-s/export-events-to-elastic.html
index 765fc42f6c..1c3ee3eaa1 100644
--- a/docs/devmanual/how-to-s/export-events-to-elastic.html
+++ b/docs/devmanual/how-to-s/export-events-to-elastic.html
@@ -307,7 +307,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/import-export-otoroshi-datastore.html b/docs/devmanual/how-to-s/import-export-otoroshi-datastore.html
index ec5e97867f..72e24680af 100644
--- a/docs/devmanual/how-to-s/import-export-otoroshi-datastore.html
+++ b/docs/devmanual/how-to-s/import-export-otoroshi-datastore.html
@@ -303,7 +303,7 @@
 

Import and export Otoroshi datastore

Start Otoroshi with an initial datastore

Let’s start by downloading the latest Otoroshi

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

By default, Otoroshi starts with domain oto.tools that targets 127.0.0.1 Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.

To do that, you need to add the otoroshi.importFrom setting to the Otoroshi configuration (of $APP_IMPORT_FROM env). It can be a file path or a URL. The content of the initial datastore can look something like the following.

diff --git a/docs/devmanual/how-to-s/instantiate-waf-coraza.html b/docs/devmanual/how-to-s/instantiate-waf-coraza.html index c51a9a3a9f..3967ed0fa4 100644 --- a/docs/devmanual/how-to-s/instantiate-waf-coraza.html +++ b/docs/devmanual/how-to-s/instantiate-waf-coraza.html @@ -314,7 +314,7 @@

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-an-app-with-jwt-verifiers.html b/docs/devmanual/how-to-s/secure-an-app-with-jwt-verifiers.html
index 95cde514f7..b885718f37 100644
--- a/docs/devmanual/how-to-s/secure-an-app-with-jwt-verifiers.html
+++ b/docs/devmanual/how-to-s/secure-an-app-with-jwt-verifiers.html
@@ -313,7 +313,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-app-with-auth0.html b/docs/devmanual/how-to-s/secure-app-with-auth0.html
index 959f7ce09c..00bf7a496b 100644
--- a/docs/devmanual/how-to-s/secure-app-with-auth0.html
+++ b/docs/devmanual/how-to-s/secure-app-with-auth0.html
@@ -311,7 +311,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-app-with-keycloak.html b/docs/devmanual/how-to-s/secure-app-with-keycloak.html
index 8fa33bd3b2..65c929bd4a 100644
--- a/docs/devmanual/how-to-s/secure-app-with-keycloak.html
+++ b/docs/devmanual/how-to-s/secure-app-with-keycloak.html
@@ -307,7 +307,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-app-with-ldap.html b/docs/devmanual/how-to-s/secure-app-with-ldap.html
index d6484317e3..5d40c558ef 100644
--- a/docs/devmanual/how-to-s/secure-app-with-ldap.html
+++ b/docs/devmanual/how-to-s/secure-app-with-ldap.html
@@ -307,7 +307,7 @@ 

Before you start

If you already have an up and running otoroshi instance, you can skip the following instructions

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html b/docs/devmanual/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html
index b5bcfb8932..827f94c6de 100644
--- a/docs/devmanual/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html
+++ b/docs/devmanual/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html
@@ -310,7 +310,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-with-apikey.html b/docs/devmanual/how-to-s/secure-with-apikey.html
index 2d74b6e6bc..0d0d132763 100644
--- a/docs/devmanual/how-to-s/secure-with-apikey.html
+++ b/docs/devmanual/how-to-s/secure-with-apikey.html
@@ -307,7 +307,7 @@ 

Before you start

If you already have an up and running otoroshi instance, you can skip the following instructions

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/secure-with-oauth1-client.html b/docs/devmanual/how-to-s/secure-with-oauth1-client.html
index dceb38fff3..62ad375799 100644
--- a/docs/devmanual/how-to-s/secure-with-oauth1-client.html
+++ b/docs/devmanual/how-to-s/secure-with-oauth1-client.html
@@ -311,7 +311,7 @@ 

close

Let’s start by downloading the latest Otoroshi.

-
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
+
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
 

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
diff --git a/docs/devmanual/how-to-s/setup-otoroshi-cluster.html b/docs/devmanual/how-to-s/setup-otoroshi-cluster.html
index cfaeb85abe..65b21842e0 100644
--- a/docs/devmanual/how-to-s/setup-otoroshi-cluster.html
+++ b/docs/devmanual/how-to-s/setup-otoroshi-cluster.html
@@ -313,7 +313,7 @@ 

<
  • Validate the installation by adding a header on the requests
  • Let’s start by downloading the latest jar of Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    Then create an instance of Otoroshi and indicates with the otoroshi.cluster.mode environment variable that it will be the leader.

    java -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar
    diff --git a/docs/devmanual/how-to-s/tls-termination-using-own-certificates.html b/docs/devmanual/how-to-s/tls-termination-using-own-certificates.html
    index 38558ba3d9..c5a81ba3b0 100644
    --- a/docs/devmanual/how-to-s/tls-termination-using-own-certificates.html
    +++ b/docs/devmanual/how-to-s/tls-termination-using-own-certificates.html
    @@ -307,7 +307,7 @@ 

    close

    Let’s start by downloading the latest Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    then you can run start Otoroshi :

    java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
    diff --git a/docs/devmanual/how-to-s/wasm-usage.html b/docs/devmanual/how-to-s/wasm-usage.html
    index 8ea9efade3..7cbe4f9bfe 100644
    --- a/docs/devmanual/how-to-s/wasm-usage.html
    +++ b/docs/devmanual/how-to-s/wasm-usage.html
    @@ -324,7 +324,7 @@ 

    close

    Let’s start by downloading the latest Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    then you can run start Otoroshi :

    java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
    diff --git a/docs/devmanual/how-to-s/wasmo-installation.html b/docs/devmanual/how-to-s/wasmo-installation.html
    index fca0af8e58..e6b80866b7 100644
    --- a/docs/devmanual/how-to-s/wasmo-installation.html
    +++ b/docs/devmanual/how-to-s/wasmo-installation.html
    @@ -323,7 +323,7 @@ 

    close

    Let’s start by downloading the latest Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    then you can run start Otoroshi :

    java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
    @@ -555,9 +555,9 @@ 

    Pairing Otoroshi and Wasmo

    Now that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let’s navigate to the danger zone, and add the following values in the Wasmo section:

      -
    • URL: admin-api-apikey-id
    • -
    • Apikey id: admin-api-apikey-secret
    • -
    • Apikey secret: http://localhost:5001
    • +
    • URL: http://localhost:5001
    • +
    • Apikey id: admin-api-apikey-id
    • +
    • Apikey secret: admin-api-apikey-secret
    • User(s): *
    • Token secret:
    diff --git a/docs/devmanual/how-to-s/working-with-eureka.html b/docs/devmanual/how-to-s/working-with-eureka.html index bca3c68c1b..2c304ea577 100644 --- a/docs/devmanual/how-to-s/working-with-eureka.html +++ b/docs/devmanual/how-to-s/working-with-eureka.html @@ -333,7 +333,7 @@

    close

    Let’s start by downloading the latest Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    then you can run start Otoroshi :

    java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
    diff --git a/docs/devmanual/how-to-s/zip-backend-plugin.html b/docs/devmanual/how-to-s/zip-backend-plugin.html
    index cd1ad5ca9c..bd7822c106 100644
    --- a/docs/devmanual/how-to-s/zip-backend-plugin.html
    +++ b/docs/devmanual/how-to-s/zip-backend-plugin.html
    @@ -317,7 +317,7 @@ 

    close

    Let’s start by downloading the latest Otoroshi.

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     

    then you can run start Otoroshi :

    java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
    diff --git a/docs/devmanual/index.html b/docs/devmanual/index.html
    index f1731fc9e3..2e3cb1ecc6 100644
    --- a/docs/devmanual/index.html
    +++ b/docs/devmanual/index.html
    @@ -298,16 +298,16 @@ 

    The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It’s a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.

    -

    Join the discord Download

    +

    Join the discord Download

    Installation

    You can download the latest build of Otoroshi as a fat jar, as a zip package or as a docker image.

    You can install and run Otoroshi with this little bash snippet

    -
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
    +
    curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
     java -jar otoroshi.jar
     

    or using docker

    -
    docker run -p "8080:8080" maif/otoroshi:16.14.0-dev
    +
    docker run -p "8080:8080" maif/otoroshi:16.15.0-dev
     

    now open your browser to http://otoroshi.oto.tools:8080/, log in with the credential generated in the logs and explore by yourself, if you want better instructions, just go to the Quick Start or directly to the installation instructions

    Documentation

    diff --git a/docs/devmanual/install/get-otoroshi.html b/docs/devmanual/install/get-otoroshi.html index 2d7b0471b7..59c37e7e21 100644 --- a/docs/devmanual/install/get-otoroshi.html +++ b/docs/devmanual/install/get-otoroshi.html @@ -304,17 +304,17 @@

    repository.

    From zip

    # Download the latest version
    -wget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi-16.14.0-dev.zip
    -unzip ./otoroshi-16.14.0-dev.zip
    -cd otoroshi-16.14.0-dev
    +wget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi-16.15.0-dev.zip
    +unzip ./otoroshi-16.15.0-dev.zip
    +cd otoroshi-16.15.0-dev
     

    From jar file

    # Download the latest version
    -wget https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar
    +wget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar
     

    From Docker

    # Download the latest version
    -docker pull maif/otoroshi:16.14.0-dev-jdk11
    +docker pull maif/otoroshi:16.15.0-dev-jdk11
     

    From Sources

    To build Otoroshi from sources, just go to the dev documentation

    diff --git a/docs/devmanual/search/fragment/en_227b15a.pf_fragment b/docs/devmanual/search/fragment/en_10ae743.pf_fragment similarity index 96% rename from docs/devmanual/search/fragment/en_227b15a.pf_fragment rename to docs/devmanual/search/fragment/en_10ae743.pf_fragment index 6110818aaf4fdd6a32a8a2baf0d7534db8f23c94..eb444af093697714f256c52c3f02643861f141b6 100644 GIT binary patch delta 23 fcmdlfzEgZdIwRxP%^8fITpZock4UYOXJ7yTY$ymn delta 23 fcmdlfzEgZdIwRw^%^8fITpYL7ytZB?&%gixZlMUA diff --git a/docs/devmanual/search/fragment/en_12d4cba.pf_fragment b/docs/devmanual/search/fragment/en_12d4cba.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..c8cfd0256978e0c7a4e22459b18e86eeb01f24c7 GIT binary patch literal 5802 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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_1a76a97.pf_fragment b/docs/devmanual/search/fragment/en_1a76a97.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..56643df5a7926f924a801cb394b060bccd5ac370 GIT binary patch literal 3235 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 V3af%S+`WBw`#*fKrXSuW005ycOeFvS literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_1ca7174.pf_fragment b/docs/devmanual/search/fragment/en_1ca7174.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..f24a4144490c186c1785c133d594c7f84869bc48 GIT binary patch literal 16804 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 P&yW8Gw>CEm1{VMTnAcJ= diff --git a/docs/devmanual/search/fragment/en_45cbf27.pf_fragment b/docs/devmanual/search/fragment/en_45cbf27.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..8aa67043e6a38a4dbcb05ce2a7aaa0ac0d7c3828 GIT binary patch literal 2194 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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_5f18f35.pf_fragment b/docs/devmanual/search/fragment/en_5f18f35.pf_fragment deleted file mode 100644 index 1ff7a5a4aede29dca2c5b70365f60344f5240109..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3710 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 F000C+vcLcU diff --git a/docs/devmanual/search/fragment/en_6f26169.pf_fragment b/docs/devmanual/search/fragment/en_6f26169.pf_fragment deleted file mode 100644 index 0a760c3189d7b9891baf207045c4683b49c9a3f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4396 zcmV+{5!3D;iwFP!00002|HWGCavL|6eibbLSWdA?$(AjdHB&W;l#9m~sVpVgsc}jL zc7qhGW`o<^Bu7cPYJcu4>^|N+$$sY?0F5RkCE3l)l*=}|8#p*Pm+xGFrJkFat%~a+ zFMb}Z>T)m{jBnf~YhBii8GbXBrd^bS;UITaYpND=&RbWz z=EkbC^V7?b3Xdc8t=p*9{rx}xyBVoVCZ}|zKv5Az^(Ih!{NtBq=J4^(tyI(Mx*e(4 zGk(%#ZFF&`ZuFYxu9o<>Q1z;+Y&BOduw^UEt8z2sSvf{Fw|1%88>42fEZv4i)1Z2l zx7Jn6Z7byBZWOp_jCyNY+K+Iergy4vn+kjuM1v@nx;0Ik2mw1QRrXe8E^;*bq-*u$ zrfrwaWIUc@yQ}FacZ>16lhDm>N~0UojKdn^^});0!Qp5>E6jSFQhP#cS8;z3 zf#)!?KT)6bTC40VPSeH0Ru{TyHm)xIxb#0%M$Gj%eGfGT%B3SwwXP$qTSYs;M&cy= z7`4uo&E$Chcwh9%tNkesv2Gkz9H~?2v4sj%Wj3mIs}`yqFOzqo5my|%g8 zqCZrd8=K!?nX-gJ>y5@PG=A6SKUUC10~=)3ENor7S_Qew<0xTiq`uN1l+$FMW1(nS zQ>fRbUDXx1B63p)+arUchZ|U;sT+*Qja_2|Oru*fSi!%mi}TAXQGFsXIN7$)#&|Vm zQ8Z7~$B!!eR(%|tipyo~-O^0RRZ5#{ve@yb##J8&Ja8qVCYtmYZkyR+vhxn^jot8! zLj5UIfB3_bug<@IqJG9Ym0p;Sg9&~uXdnyV<6wyASyZlU2h^!QgAy`!Oz zgCEsrnmN_^Z88zYIXA6LPK; zRTdF)WP1ysxN@^@KYLc-kzla`ShW!-ZTplBwy@2z)MB88ofBSx06pdGiG?C?hhj+( zyfVOPK{FRHI()u5y-=hI777F?bHg0+o$jk+ZfXGIvUP&cVHK_X{yT&ii>P!Xk2JJ^ z$y;3lmCs2_jBBRoz}zi?d|FLww*k2XFiHncCc6@~@yhU+TN-jg)GZ*N?Sc%Zaitll zx4<{B&m8v+V~f2WV^U&+3RSk?gsv%)zW5ZPQ(t{_4XwN>VCeXZT-YA#ey z^4v6yx&qpg@EOw!Gt;XQoc?*WScbro4xCv77bVFz2;X^2ZZWgoRAkBYjXca(-VPz@ z+@xDbK%Q1>0HLJ3XeK%ge0|1?#^+1McVseo?Ls)Cn1!7$f|!;B9n308r46hFZblsW zn;rqOnBdVgodf~1_4d@WCFD>oBoC?T>Cgvv{G=@z4evopLc>lwNAeUC6SrYDW6v#Y zoAN~VnqYJyY}u~jfK6g2bZNqvlg^P4{}~6|Qs8fH09Xu0o`T;eo5hl|&C?w}Ue<06 z1=eceiWQ;8H*i+U0EE#>ukD;o)BS=OZ@V)kS9#~i(LLF2s&pS@52CCln)3mi%8*8OqRZ#(_+umSe?H_&kUq58Tpi2Ftr@e2medI?Ak(i8(cne7^ ziK_TrZPwPU8dcir)`vAKbw%MSB)@&11KVWQxtAe6@S7VL_|!nHGlJ^~&jQM73C^&0 zW{6(@`SSd|a??*FG)ZHdkfTO+KvodSRpp&%HN`|YEyVX*!Cw4oqAue0ed-xmOA(;+ zF@}QIm0AGAQ3VZygc`Rn00vlbi9}Yt+RtB2XZzFaS-ziVhi1B;X}$j}J9>8T>c!Md zUlsZYydVQ4-hNeV={CVuu|LlJYzV)707i$DROx9McStzpmA^MNxrQaGjlm9T3MRxp zap6NWXIQDb>b0{4dq|a+tAe1x-C9a!DS7zz>gqy$@EHRs9(6)klXO*?a!4*}iSaug z-aI8V7rB|TF`Zh6>JuUOLm?nIT3Yh;3iLEh;Tcyh@Y0{aF{5+zbQzRzM#^|BQ=7RZ z|D&DWi+E8;ND|eT(91b1iwBQ(LRbREobS?}YxNWSOzqu^iiE=30GtGL8hO?cdAc29 z6NWT1)c80U;nky0uNnWyg9~ab-is&S+h&E-{U_=6v4~li=yNrk z!9&zAb-N}rNx%5!485X`k0FHUM9L$*UZZTkd(Gt9gksmWS}uniLaBn>RNX2kP2?=> zki;4$c6H}E?qqFsN0^&{HYurlJT{>yvo$4E2;Ee4ZZ{RZpLh*Psum>G1Xd+oR(lNJ zHwG34P9H)v$xLJ32Vx1Geo@#DZnTxBz6-$XQBp?0iG8z$s|C3z1;#!hIlEYaWn11I5N9 z)0bP*>)!N$w}_xH{dq95Jx0SfUH7OD&IC>p&ges-K4zC1X{j`ydpGGw+d_78Rb;$T)tD{Qr_WC{yb-Whb`%4>8~ERr|w zwE;sYccM{64SjR}jT>@vbQhPt0rznru_ogLSZMiVIzwzTO(rR^d_iu6#MNADE_>x& z!o+H!uL*u!ZGYlDqA~d+H}7gQGc~~BngrQ3`}|z=?t=dP+Cd@GAw=I%SFKF(119xG z)@+Sl#J!{+xheRg`oSdau+W&P?o_ihIb@Y9kp^iqY8HkopYH-6HaQNMQ773hI7O8N zj+0a$9J)R*=Nx`Jak8gJt@EB2NcPQUM&=X4rT5!Nr=IJ|3zubfMcb!zhWg-?rnehH z;RYcaDvJF!AWPb|7iV|ADR>idiP;KhF1n?KEg0RaC&)LczTHgJ54EKR{TCBOUB&T+ z9-n$SAoZof4%eS>T4F=`F5b-}(Bi|?^bUa}U~$Y&#Ka8%j?^Blw^y(a*6z}9yFxh@J5;MB zhY4PZ3$dhzROCRE+vgvUyDW!4f`1$iCCDCa1xMI zw;il|!|{S(AAce|iD7*BI;v3&nsy}zZ;EVfV;ENOh-Wi-X zP_w9Yw}!>va=zpjkD>T?R3te}3^7vd^4kAObxPmP^VCVzx9%JE(k`?GX-SMiP$tnH zynKH6?D*x&qeGIo)ouFnfL{{&U$<89z5nS`Yei{h`uyebi=*Sr93Q{T4s)$D{o-Jn ze+a)QuVu=Y)XWTP z{^Vai))|qZ-rTQ|^R+$>n#04HIXpT*1LOHLJ3Klnvgy&QXW8NGXbJ%t{p{I(V850r z2mYBd3sYnWx$|%w*9q)_)IMyU#S(+L=$eE6KPxwo1Hurlwm9kQ?jG*Q@iJ+rw|7}+ zRQjd)W5PHNu@Dd*9F9T!b^B?X(-|&u!9I2{lij zE4u!xWGB2pI)q;&RQS2KkSB5vSN_XAY=tZ_Xem0*J(>vg!;s<5yNnv1DSsY{JWPL9 zqtOWezT-*#T8?4bhXV%Fcpn8Ihd2-WuMurRu)#sWl6uQcn)D2PUzrlL9=iMNPq#?d zr*!5nXDw7XhEgsp&Gl)ItC;$11&gHC(b7yEa$CIE$phugh+F2IS_Fr^4tZji5p#Ff zmt!)`Ht}$(t3h2?dZfihY`MljC_C>;(0FLcAJTky=zCz024YCyCkfyH9=H-qqw5XI zA+DURB93f#7KTYK7dtclD0AzIi15auT6hyB8NG)9r7<;(m2RR@I!B7Bf2ZpQ zMUG9x%CM{5=%xiy>>39c81ucT<2U9cL%CToTal$^G@w_jNZW3^Cp_#LPr;`8i8{sZ z&az_izt>-h?4@$8N}7vt62uX@>@ee`a>uHae&!A-8wj-W2o(n>9MBFV+PH8oQ=e%M|>^eXA%d++SW)uBZdd;<*rK>Yby zt>L^ly`tck>0}kXYTKa*IFgQvtAeh!A>qvVRal^?c9lD@NASQrpEt3&=-4laod0C)R~JE%R1P)ZhQ>f7Ho* zj^hyCQIu@poxX3CD_gb6ZuLU8pY})n9TBctW;3Zhed~@LIXPn3s!+JHkkiWccPdtX z&$91vh<)U~>F#k>I1ddsLmXeqxC`hNJAYd(_aO5oR?o>9B>XDe*W6%3;lM1(Ec{J6 zub^624d24CT(c&f5Xmxvi;|b|k!rG5azoSiTEKs^-RDm}jrmf9&69J_tKB2Y{;^7I zm~c}`?)t^MlX^zYdVKMBb(<9jEs9@vUDIW!_`X-5{hK$vefnz%;h-5Dv#b2SH-Gt1 zmHvMBIP0NHs?x&t1|6mL*g+IKSQJio?9jEZV-q9vtHmhDq{C^7kMd&0|7fpzk2p5j z@-0~J7;%NAm!8Y8&%Hf6xfl(G1G)u#%>}|>a&WZ&ayURy0F!A36ZEPFIPcf^H9*By zn!#l7i1!XK9Osj$Z!q_-KM!!=x{yoig<4)uMt)+k*~wZiKboZ|z2QFDQm zJDBW$9;Wk~^*qdf_p2u*ixw_#9+-6S@T~M!de*`iAb?2Zp-bvL;?EEJ#3d_vgnkxB m_+{>)dHh>x=5isoBl^${-M(NBpMU=G^Zx=;68oyBFaQ9H_nG$q diff --git a/docs/devmanual/search/fragment/en_70551fb.pf_fragment b/docs/devmanual/search/fragment/en_70551fb.pf_fragment deleted file mode 100644 index ae9b40dbaa30717363d91134432ca3714351b7bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4549 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 b@_!p01O*$ze;6G<{QB?@x{jC?lL-I-7X55( diff --git a/docs/devmanual/search/fragment/en_83e2704.pf_fragment b/docs/devmanual/search/fragment/en_83e2704.pf_fragment deleted file mode 100644 index 8c29834a487e85d882bd06997e1a7e743f125271..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3060 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 I{Pzz40Eo;d=>Px# diff --git a/docs/devmanual/search/fragment/en_911d366.pf_fragment b/docs/devmanual/search/fragment/en_911d366.pf_fragment deleted file mode 100644 index 51f0519cbe4232bda67f239300c4e15f13a4176e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3235 zcmV;U3|#XciwFP!00002|Lt1oZrex_ewBu>fM*sWDcSO+odKdaPU0Oq_SniX8wNs) zY)KuHY=(zqO<-VOV_$EdWWVa>C0Wkl$>h&Yf*@pfRdsdURaDHUVj?4dm8f(k*^~35E1Q+C zHhZoXEK~pe_kU8Goz%n_XRilO*+OPBc4TONZu?T@*ocFC3a0wSvIaY!izJc0z+q{| zo(t}agk7mnWHJ;fTdJIee90Cd4qjL)ygb1X5#w=e&Hx=TW`j761L^Ths)#+6iCFL; zU_ao99|7$Xc(7#IOc)X}Te2h%aKa=}p1@ zCLy+If_wTJt8o<>RkFiYqVg#*XD+SrS)SrR!opE!3^z}STQ#ZV^jJ*je7ZH{nSgMyujm|yM)>s}O(`y;)Gbf5JLeK=1BN~;Y9f(@8;$}ClJWWVPL+nQxroc0}O z3Weok+f$))HaL0elu%A00>L32r#!=%clYdW&+b^hm^)2sTXc3*-47xtIIP(3v-f^5Jb_6|YABn_*CFB-uCT?QL+h?$af_%74MKYJ3SfdXMoJntnV_@7c zl?BHw$j}q2$of> z431uA+DTJDctXwrF99*^5-44*x+*iS!g9iKnlVjpo%#6#h> z`gjTnPz%6qwegO?KrHyQOOU<_IXs76-xBm44Y{p%?3OUc+`Dcd!Z@cQGl+U&hEUfy z_l|DN`LRkeg6Q>j=rs#81vsu?w+tuYFK&U*5lv)bLYVgcAkcDZm>LBqkHDsRQtSaG zMLe4-GCCa{0E@_)vKTpqLQ)k{3bjXw3evkS;Tv(!kVYhtnFwv|LSEC#SB}P+uo-~T zCS+iOGxJ62B{DV;B1$R9>ySy#C?Y-%3QI(yiG`Pu!8^pRw#-SurN%k*&=H&bPXhpRz{?a z(n{O<=Ixeo%qbai;VWhQ+*oKd6Av<>M6u#k1Ttg2jY5M~NXZ3|O;wGt^vB54 zhE;O1bf)saM=Dd-j8uj=59J21B=&!&liv#QQpF5qER8Jq7@3DeRY=uWUXB}Prjl&b zW-|dzpa9e*m`;jNlgIRZDTUS*rctm*3Z@#9K#00uUXFzd0#UdksxJj~BN0=%TN;YV z?!;xSvW1XH^%SwnT;C?9H7HRs)e6cqTqeMI#763>ptog&RaD13XWpRiWXSIEspzC~?0k4(mu-slu*LV3)_gzC6Afj$U4#Ao{tj zM<_|fhk_g}=v4&3Zl~LhXQ&PP-FDUoNs)MXYZEU70?1Oan#VX%bLuBT7>0~-5*8*h z&^gUi{H3B-EZ;X7m~q6;ti9DB3mrefGuF_&sGL2!!aA$z4l`Gm49H0y`<# zuuErtSSot?=Vl^jEkpd3nZ9cj{FYwP-t5lbI?D}wzvyV6maVReObMn-`qj+l-RXWz zm8ZiPIhsb?P_LpFMRB*JXrxC0yuhworiIEU>)jYUaH@&v!O4pFYO}&h`CgGN?||yPW9+YU-k9v(nDNXpE%m&#Uxj~ zd!PR{%%U9a0HrAZKi`$%b=3-Ld;hwYY|Ti%tRK@bX2Mm|H48gw@ezaKYI-W_x+ibW>%@24FvUSf`HQ0 zOma6BwnZ=UR?rmWU4>+6=dj2-n8n^=_CQsc+v;xb+a3Jv(zL}WzRzCsgvOvYyHLcE z>cdt?;R!;qr(@eQWF6lwa}KHf7&|DS3*d8_L~WZOD97m3(9?Ded{#l`8#(O`IT z{=ymzhl8VEtbYz(4T|*rj=c>iyMndOUgtnBNFT32?e2E;I|4tWoQ%gXr8@gTd}@5S zG%snT0=`C*Y`7!-M_e{ASjCd=LPkfIS5A*4W+h(Na6p3|gJrmbfgxRa^+Lvp$402P zbTQr73wSimF(|TV(ArpsWn|>lcMy1%Ap(O3(O9fFY^-@4<(kv&bUIFVciZXi>^pmn z?Te!mXV2cXx0{D@1QR8>6+j+=V{TanbQ8_j(*gjfV{u!Kd5$C^#7L_xAO6O#lVE!G zc5G$Ll@+-Ofls$@2Aj3x9@>?3DVapOoZ8`^JYFfD zSDANI4B4@7+x9oIzS5dj%wzko+pEn3eZ^+m_*!Lup=Q5Sd8Pqc*I@YJ&A-H%|NA9> zHTZBMKg^#`{;&o&(rjvQ`*Rx9#YRJc&qO552SbP~$}o!AJn@+paC($u#!~Z~mc}w5 zAy)IFwyueYmO&hg3Rm|hE2De*xRl_U)h9`6EAljlCu?<9`wPA_bMgjdoyr^2!beo( zGphd%_Qrd?{oZcJ+VQ-7tGCS$EzX5!9c&-?+k0Jp;O+NJ{r5-@{@xihHD(_cp79V0xO4clEbz^;5MDt>a$eKl+ zI*Qff%gggivkco`VeF5V8buEyMc}8J@M`a&Pg-{Z%}3!#1xTl08ZKO{6txGfRt{21 z>%~~6zXWey^CSNa=9(|#4+aoXwUoo1466Gxm`4F%7E143{vmoI4LV4 z-QehuRdQd4E!sl_DK<8y3?sszy!wNxwpHc+0bQ11u%J}eVWu-n_@daUg0$Pbulw!G zSodKfaT{5|Ee*=8GJ{*G&#e;#4Vv6KNw^P{q}#|YHO)H1xDCO&b&_#68;HA>dt5SA zcP;-k7Ik&uZYp?J_g9PF)xEXCca3me{H|`)1@P+n#v*tXq%MTl7+H5^cP-DlcOr~i zCt8=B*j>-F?&csuaIPDXAKbDR$c}yXYmQIEs2t5N8qka{#5k43o26nOK-Y&UtEKGsiOJbZp=W(`BvsR=;!GZsMo&X^DUK#Yl}q@jWTM z2dR5MR`Vx1R;H#?h@c{J-!FDnh#%~&632(34D0vPw5- diff --git a/docs/devmanual/search/fragment/en_949c56f.pf_fragment b/docs/devmanual/search/fragment/en_949c56f.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..8b50137f5241f27a166eba99007fccf0b4ab7164 GIT binary patch literal 4548 zcmV;#5j*Z5iwFP!00002|Lt4rcH2mDew7~X50VoSlsq#T2fCc~csye}W6QDROg5H} z+yt5=BoIK*fMiCibMhK_y*x?2s_F(nNwlT4_wvJME+Wv~)zwwib@{qc=((9CS$q;l z@nx?r(_Yw{Eb@zSm5=S@LfhrIw&)Wqs%6?6^`bnhOje=SL6w)eT_oz~gU5$~@{fUf znqR0Y|G)qFUmK`5Hp%9S$fzQ%=SjBV-S1}l$Xc_UrYkl0{n6ozp-QaM$|lPqHEOA& zMUvrf9-CC@ELP@&iRwyE(KyS?rLNRPQZ3Mg7BFa&bE9lwqGXmtx=QjaP|qr&85^6- zGZTYFH8rZpOQteXQNAp6RE;|1jRM`gRGHEEAIJz%6WeyZN~fjK@k-gcAYxWk3**LP zT9wZftw+t0)L1oLsiMr!lbA#hC$pI;Ax$p_48?>I76Hz5QKSj@1dTEWFPQUBdCje) zFvy}rrzx5+EVrg1vH;CM9pxgtWVT|CFl}V4ZTN}6H8V)dWfhOwH3^T#hAQ{3>O>__ z{k-H+A#^jV(^Oeg5y`a$qsf$GtEu0AIEaE94`m)ZdWXS^78HAF9I3@sD zUTn}#Ooo!vd=ZqdOq`&BdeWk%Wqx5zxjyYhnLu;+RXVn)_v_k1wMR*|`ZXV^=lWdR zs7wl0Z)16f@`@~p#JbSuq$Y4%!TQdeu*pu6lp2zX+IVS$RaZ5dJAoggsv7JjI(36F zgw!*Heq2MKeljysB{OU*T~>j5Ov*95L>7k_H>*)rbgt7R)}R1F##S=8^YTp*uM>db zm8M8_#494v>FCUWSJ^hLjRx}5H^c70&yx=YSs3ldY))Ga9hJ6gT?6%KRzVkT>24m3 zvXv>UXeNQhK&r;8Srk`W<4AckA{Am6d&S)9?Pd%~V~mMZ>-_Vxci3kgaqU z!OFd=>{(KuQ9S(@wE3pdpy zw~kMY;91r9H#N>1j)GG-mHwit3L8!)bEu)71`yul#iM7xOnk3NX;PyhpoyPja=!h~ z!S)Zq);KojlPYj3%lc-?(trLV@V8ox{PUo~`LWg8=eBxQ*`@8vF!L5C$+JE9HN ziNUhvHIg!?e6xU9Q@K(mzXEhOLN={@oPyPSpdhq5uRryvk+4&^^EhS#?GO~x77QF zhK_sh)D?|%uEGz5an4O81NjBwSk;;LhHhkj3bWMX`|rP_$0}tC*tkYrDa?d_unGl1 z&60A-8sOcZrY;k>k9++kFU1DDCx@juLy*nY<{gQxn#@LA=@BU(vS~vfVkJ{!56V=c zg+Z94$uTRIfF4@R-Psdap(YQdbR;1h$w*!a^(N-DRNY=A>n`<0Af5$2wsg7MEly^s z;iSt+Y?iqseG^E|R^wI<^zvitX?l*j&b#EOYY|P;7+xbkQz`5&?8q0pCCZBx5<5i( zZFeXdP!5;aJM*4FDJ9?ohs_7nq@vi8$5^64|ruCs$r<;ahEBD;qT!2 z5sU2K{d|w5M_th`iYh&xQ+Wo{J^ePY#%>F)%l(Gzp>vus~S1m z_gmZQYN)PupsR{pnJhYRt}t+JD!|?wU^nO3Rzu9jOyOy$_V!gWgBQ)!-X8DT5^m*E z9h`}@@3nB+md(0f=1AhNhTu(^l)L>!G(S(6QZIairC(lAMQ~lFBU<_Jqmrc+jAS*C zm0ttQf|qvHvF}r{Zwh4a70tHs);n|-Jj-A> z+#$sbHYoJ1Q|3FUtIV}xuKtd4T8vGU>XQ6* z$?S$y!ZfX)tz%BCH}d?M77%`P$zR$$?mpW;i0ypcu77ip&vOovJdl!5r&~!-oq}DqEb$eFKy<#@Q(2O#%PS@?f>We@@OxX# zZF;=Fm(U;V9yrY zwkF_BhktD?se5b+)ISVN$at5NOCD`l|3VT7S32+bN3O_9SiGGCfB+uEx$ zTrYaDH||sQx@z`sS@tHeYznG{#-2;r3~gn4OXJ6j;G`c&r zTQ=&gMCnyTb$_!q3&=1s{^=~qoPNm;oz!%-Xy1MLvRP)y3(@DLlj@Qk<108}aj$Gc zF5h_BXxfD;Q3hihglrs9#qE6~g9x-AR@cBxhxmtz499lRj&);n8=gFYx8W}CM#DB( zZ_og34ai1_0E!~9-x1yeO5;NsIFXxH3%^2)9{qMm136H`bOUVSSopm~?cTA#t2r?n z?|i1Z!*lCNJ@V%c7W^SfGrAQ_b0z}ZE(7>K*Hv=>04Zp@nC6{k4QUFkW?D5 z3ps{hPWR>gsiZZbBq^Jh;-VfO6gAmpvz!N8Tk7CnKkKN1@&@t~e8wP$LLH=zB4m{_ zpwt;f0n1Z#QP)|u9K7v8k3R!IS#S|Af|Im!FNSA=u%Sw>AFE4 zOo<7PSZ`1xpR&r9VTUndsLVR7%r!WmY%)=U?V;-6W%TC3@HW`2=EIjb3?rD7kyDK@ zd6FcZ=6lL<7KS(h7@%VCI28hbDI2JY zfEnuwtdY-@yDx_#lka7UvaL2ZaRr{-R1QpPCk9z$PUT=2z>g(aTgvH+lWXN1x`>#; z!HCZV$bnxT9}C~d?9h*eU;Me+0N&BAKwafACC@$sOU@KdTT_7IKUJ$jEF{|-#p%xG zN)|($5S^Y{9bKv*2sRY7#P`kG)~jffRxaBd`YuAdA*oMRpsfgZ)=E$(&1!99t)WI; zGasp&c5`2Z=nfi_V*S2QQJ2w1aYCxAI;WJ-t7JsV+yqakKmQrY3YneLo6z&p3~!Og znld|Tx|?RdgfhQwjW4-1Zi8ml_sJJPXjoI91=Kig&}M7og`7ekVN;F}bZ3@w(c4g* zPvJL1b%HUT7C#8Nn{bZPpv!(j!eKgu!uRT*lnYQkUf|7p4q#9%R|XqZyN<-La^L?I zP#B+1LKUg@$&^DIB#|lVk}rbzM26g6d;Q1dTMh-G6pC`a)}T3m1rDitOhaaP+?rd z-#4LpV5zavnr?0c>ZPH)RU?xVJGXQNhD@8~GtZmca;eV@vuX2LbwMs4RoA@K%Oxj? zYuCLrPdA2~>$V7X>rO`REi|&tOBk3^>WKn;7wO6`)U78hQeq zIjIYf)n@%}Do3%!ul!v*auKa!^jZ=xdgGf|Vh}8Hx>e^cr|_!f0-Cp``g(pZMISx0 z4Rwyo8>r~by^*5HHAvu2iq@nd8VdtGy>w#d6m0#O$%KDeMkuM*u4QU z?J`BiyLRuw=`P7A%{fj*A}cB%wveJ-tDvrstVkI1HpAD$np;_wP12-VwZ}Jvkx0>31l=4{x)2praNDy?FjeNH zb79xl1e@>bhEP}HZqZQ=!@otQCUnhlyn!H5Bwnn z`k*diVg>oAYpgX+VfUM#4;~-=>D3cOqxRLV{{xf2pDL+RvoFh)$#*C6PVG)yAIijN z#PP8uo<<(*5?F9mWp9&i_VpZXmCHG{%t zjaHM@-XeTgqCO)k6bl-#rqyHrlMume)i{N^zRxSEFzkn_PgOswUry(Ad5 zd}%^`bPe+HKdb0EKh;e$*A30`y_C$HK;oCNSF+gWIY$cLP zp+57yQGQ_A%^eOv49jG`sO0S4oosMQJfm~5GLOX_IxWgQM@$fbGagMcx9-EI$IlOW zSVl7Cgo;*T?1QZbTm5&`JI=Fyg<@CbbZ0LF6X&10gsmpzl3rF^3oC+E~R#!ui@WB6{k>oFQ@ye5&+=uwZ+BN$FE+`1Zd z$J_I{AOGUcgF2JOd1N;Xdbn{=H?WVZe2#RdrLpOooHg1Hwysf^MBpP6jGG>Ao?D1+ iKe#W^NWMU5sc!ltVQr%Cwtu{OclG~J*ERQNUSFZGt>RcovyK^wWW%HgRg$U5OvPNmZF(^$VTJ?OzOo6(P|^*18EOazKpYmlT_wMaetS(Lv>0 zYqK<6A^80=K_Jst4_`b>N3OJ!S_&&|I_gN*i_eqA{bU{&a-B}49kA|Z=O02aShzDk zqjzF0D1Nd#CW=}$r^4Ee>54Dg;elc{?uMmb1JyxS;qsT||#V3yR&gl^Kb) z)u<*$Z2Hcc<|6WpeF!znj{EoT@vGBb0t#2sH9-^p!wW*7O4Zdq1HSsT_Hp{8-gm3ib3V&;J~_?pFW|H! z9u!_kZtW=KykZ`oHp@J|jiKSmD?e94_qLVkb-|rdBRAFqFQemsVZNOIaQf5>en7|L zA3zO%oN`(|_)gf`3~m274KzB|s<{Ld^_Lfsbz2xxNr|`-0|zuGuUrZvL8&@ff|3T4 zJS~NQbWp+=XO)Ww&m}r#=+0gzSQgAUaHPSwKXG>q${pVNYI^Sn*$TLCij4kUezBa- z=SNg1+iJ1RsMPZ6h~D+qsd5_+iZaUaTXsh%dbMg|OwQWwiIr5V3QUXnyjWL9KuEQK zq7sK=tA0iy-Roq11hcj2SZP@@(+LJf$ha)_gCh_;u0{mF%cXHuLRcMW3B$Ea_;Hft zeE|M!o{o5%=T2{5D!AvYEj#6OIGo4o}sgr=&YrZy0DnW z;P&FU-gX^qEbZBjGBq+F44(Ke!vcZ?T8!f-rh(y(doOC5J7->rW))m0WGQ;>s1>f_ z6$`YOmn;tjUwa-S{P`c-B@0=b(aMkMlguCAo|b=cuk<`2k$cgy;bpBDIcMIxWxrxV zDYTXQQ$c&)W;@Ld`bsj0On{ze$nm4m7e1((T=#{y`}G?p$g2+sId|^W=sDmJv@lu# zh2uX$(SvkfbwSNAjJ*`^W}&(uC*cL(4)HS%&V&ae!??KcJxujnGQ;L{m%jK`7v!_~B|=;ZK=Z2|6L$w@Sg%p& z*gjFR6RrhU8MoDwiLLWn1L%T)%UjMy}LS+v>I|tB0&>O}5C2 zwcF;w<-YJLbWQGpYn@-~yfmE;suk-{n{1Yq&IV-zI4*k%dVBfiO3?5s=$+eAaR2+y zf4!h97Dm#JpJ|ZyQ=dM%E+}eDx6;-x=!>e-K|3oAy;HJMt*7^v|Aw{p6gu#0X=b!@ z9hI$;;Dy*;8oA}fwyUnLWxEsfRyD@$j;W`u4r_8&k%`JcQ_&f1UHb`|1zSUsjFT1J zNh56u5^ZVO0vT2o*6Kj~4{3D82JtDJ3DOwJzR(?%!!Nsnuys=8Y$R*dzdPjvxId`C zFcR3>z#Ns^TGq^LObS_`r0?Yq+$<4P%0E!&HPVd{zq||S87vCrJd0ATGBjEW)QvR0R5{+xa#jDfT^Zvl6lh^6w zsKSdr#Cb$;iqrso?_JosgG+sMXQb{M=T*glTf91bbynd2*JsgXem{@z=fq}!nvYRj zVGLf<{r8U_IDd|U(z}Uh`lHwg4goE7izG-^C6tT?yB!j0YFo6W$L;3k7%oYUu(DPc z={cV#(nlzt9nKCwpFJTnV%-~foMPgsIK7B@iZq>{{aKviXU@jYPmaWi{r3u8srnfJ z9tGpq0&sQ&fb(Mjod50sTynt~+1YZfs=f!J7OmXqRm@x$AsCw5MyqYgn&AX3)S_bp z!lhny?No?YsoKuR()KBfU9Grj85Fa&OV`%1r+LO($#OlCLz9E#x-kl-xUPNF181~F ztCYxR80i~STGjVSuyB+=EMOLn46JUDVS#E!i=s7CTg$`k)w&)Re$~YkZ*Dj=b zjrLiEFB8@w2x&wtHM$?CVKoqla=AM6(4{kw$w2i}nHFgbDa0r~+*;(FzvX12J9J_C z1DC=n#bz7IhAOp`oe8&h63$qjXyvC!o;WG!T45L?Sv1R^*Yp(SD6$8YTG!dCbx?~u zYBzw23FowSK8OUK(6F_|Tu;5}89krBm=d2oNqpWDkM=ohRS5%64>tFLU^C||2Wf!0 zM-bVP0FOuus2qY?x@I>J=OyJ!62&wGJ=t>v$!Oc%zla?DDQn6bVxSY+`G!7w-fXKE zQyGh*^KD^VDNUh5IedCV(mKj>YzaBTaB&n1KFqn$w{kc7H!-i4rSrd?c}FxNA+bCB z4mz*n0))wjHr8+(Ln#i+2xnhVGhVUyr8BV+;%zp}+O%`=SVrqnrC#b1W7Tw#P*=!y zz1&TfIj;Fi(3^fA;NvtGk$T8KdAP#Mq`W(2@P{ATNsAtvBmX$ zy4G#$S{nAW^eV=VuidcR{foZ2yt<}gP>lQfW8FM8*qkET>QCbEc}F+do76G#uxr$U zWYZYl(9m^?@4d4(StRU$7#cCln2;^sosJ7CY#}LXj1%vi7Hp4NqT<$_9lDe!AhB!~(aQ3!RSlE}nxw027NZ`k?t|zBh{L_5zqY)$ z$%@o<23Pt_9A5jCuZMLBL`*qks0V_o%$dvdf!xG9{_ zvuE^C`39TBSf7AI2yqOSK>-|Riaq)PJn(DR8PsAInl6kMMThKY6KXb;g8r#sIYz#U zgT0-s1Uyp@a72G1#bjr~CQrD(maYXr!Qe{yH^hEH*B`D3Bq)(I#5>OthU>(awrqDz z5V%Q0cy(K1Na8a6^3{BnVF-cIXN!mZEUQ(Jc*=qfM&Y*oLKe|lP$7h z?Y4Pvxi7p5U6Z@uTIbg~FHPrzYQ;L#CYxoYvq9Maj?12c-d?`B5;VLDdgrzj-2eak zKQHKtg^{%5XBy=F+^3JO3yK=kt+e$E`l70I(9TLj@06@m>*>AazhSLCg%12$ni=g} zM`f!dcp3jJjH%kPS@(+|d8UiVp=wuy2;}=D-f{)#uC|zBA{l)vQ zih-(V6}+6i5+m7O3S{R19TTOxYMHC?i)30t;&6f^|A zc9kM!HySwB$S~SSMD@-vbTW_*;5CNs5|k-40-kRqWU^XDiiy_<-pKZ=du5YSS$NP=WlLdj^b+aaN*wnbZd+-`1;;gaMCD{FO; zp7VJkeT4Ga;p_nP*%LA&*1dtpDJGtZ(~Fp=NYnY*U&JYX=4|}@U$t+(aMcp#msdPf}y!>wA!Yu8BWkbEjl(J zT3)~>)J;>a7J6S zN{M`ik-kBtRehfX3s=d|2}zbEJocmKL##)$#F25p!Z{Z@4`XH1y89h|Ll|AZ`6f2- zp?`yW8J(`2Mb!t8Z4NxW$x~ohWaUaW0j9T|<@Wc8o}&U?=nzI~iym>Oj05t2>q4s6 zXrER1GGQHpkVeE(qx*3hRs(@3m#ae$T{;7q3{*drX_3Z|LX6_WtwrAXTTUjrLl>q$ zaVeZqY__3ns8UPWnQ(h2;f&>pR(^`)iIak^6^1dAMYH^QO;1sdB70D&b)BtR2ertf zb_1xGa87IIgGk^B4O?5x_0*f5(ewF>De>8p#OFQnXrHrIl`!!1U~?}BHgnE$kOr80 z1d%NX@QAd4$|0zwYjy*1UQ)g!QA|V7lRZa}jJDnVi^$QRvZlNt20EdgZ|Jk<&9-_m zm9Z!~-xkJ|(iAF`!>30it)o20mXI?H7e}$+!<-9!D|e%R6Z2|WI{*8bcSIu+61&46 zpz}H|K$v`JV-2@4l;W_AaP|c?;}wfvIujcq-e$wBO*ZL9*R!tWPb%ku# z%iUy|PM%)JWDDrKwrsROs}jLE;}`e%gKA%EG{)GF&@i{Mg<*IeTU_6# zYu&c4rD0D?uVU=@+6~Lyzv-LHt7{qt#kj9O*3DCc%_*X-{v-~ccXX4zNgX2(yGAWY zHjUv84PB@B-aC7fMZykK({$vKbzSCdD6^sB$%$r03UfZ?Z=o++^R;o_YGdGq7MIUa62+;sF=JMA=Vp zOPYC3GsVl7=lmK>ln`2keNVln{@!sAtt{_a)j)ZmNxI5rG3v4EK8Rj`INV$MYs-6^ ztVmsFaHY@0;k92m4!(T$km$wFZ7O?&Y#)E%1p)rF7^HeY)|Z@Yyv1^N^fA@AKWd)Q z7=HW}8O;JQPiB(}L57w}Z+L>Gau^0c&z@2`4$N^|l;bvr%?VR5)>WUlCr4|9o5Be_ zdqy9XZ?H*>^$AFX5XWE{6u@z&*rOl71HX2iK`nNn>B4AHbjXf2p=LuV=wAw!W8|wi z*xSiUz%%s#NAxFBOm-%0@`U?q=~@6346c-aN9-4L{o#s0f)YtXyz?w!xK4a&%XZfU zftxghSGOgGBrel0U(IJ3h7cHiws_djvRVa+r!44T6mHv3-j*{E!{W7L&Tk(w+~4YS zL9g@%YIKE1w_N7fTQ<_?_}Jk%z*v016Zc!TG+UhRXH$N^Sq=PO|7xa*QuKcR1k>yN Pcl#dzZJY1Wh7bS%_ZV3g diff --git a/docs/devmanual/search/fragment/en_9e47152.pf_fragment b/docs/devmanual/search/fragment/en_9e47152.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..96c9e367e025192410e0f62c12c6eb3e73a2b090 GIT binary patch literal 3711 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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_a340d04.pf_fragment b/docs/devmanual/search/fragment/en_a340d04.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..69ac0d8206ec858e629b3e6324b88c7298fc8125 GIT binary patch literal 3060 zcmVeFY;c$m)XMW!ejL-Z!7EOMm&CX`g$nR z*P(cBSHjzW|MOpNC|cB2>jT~zuDSrQRel`6mBd|SPLf}>8Zt@wXKEo zvhqW5Hsx;IkrkDwvw1qS;i%j=f;F#uiZJh4y4kI8aR1LfKW3Td8O%ZOoBghc z493=zVP+SjZ%@vij?$`8rE@KvcB6F0XnAyhcyw=gI8b^y>PkDHvx~+ISQ<5KZe}dr z$)yy7$IUjW7N+=4I=8Zw`l5^<#DJ9B(DWrPF-guXdBodRu=Ii~fg#N%d>DFbb2ol) z_~0_U8hHWV-A(#&WD&b|4p(Zr6F zbn}Uol^U@e$9JFQCc6^0;5*8~d4*nuqr{~4-WEgg%n)EiAs2iWjINS0uL(;V540DH zwbRwoWV%&DNGGNhM}`787Sa?hEKc{mFjZx%VaFA;XCkvJ+Jak~VezfGb)NJXi{15o zQQFFn#ofEdeQ^%c#gjr#a;ByaAqLOI zMej7k`M|GBs3Xe~zLXRXqj%007d>8BhZ`o5ktPO4jdz}bO)8j2JCyipAU^;6;K{3} z2jV@>G3uf>#=nhMFM54^B`>5GkP;py7t;&c8$C^5f9yzTt-I*`BtFp2=;*XD_&aRm z2g&jA$Q|d0^XUR;1z!QADf3*E($6Cm5&f`uxgg5t85M$2!v-qcXpcSxgVH_WE$3&*%Vxgv;e9_ zNEEQA0!LOR^H}3VC5xFRE_K?oA>eB^Bc5%dQGWxFPvm6hY>oI?NNhf%;VAn+7$bm8=P{Xz0YZ^Uj9KhZ0c zBU3X=)-7dj6mZ@>a)G#zIA`5`xH6@_jIOz?^v^Y**?nJ^Bm4pqNraYDG+pu01>%cY* zhT5MtJi zh{oW_6;T?O5L`b7zn&0n^14_*q!-R8&DzqP*dkxIzMUGKD@VSB0c{BtEg?T~dHW4r z2^czle9hF+e0_-+W#LXIfwU9sZiyv{yw_qDJ*CLm8wI&u>Z+}HExuJym^6nxgiRew zrUopM>D&TQS3-wXu=8VNuX#gkUIUgo&O{9zP3QD0Goo1+Ox0 zf(p1&^eC3$N2~=O)=q~zMOlJF+RH%I5GwWYurt6D(IdeLMHn;#gK|^SL@BlBa7zj)UeFeBGXRO) z7do8;&kZbc6~^!i=mTt0q4ey@c`SGic9FCYV;~8#?=ZO+!>f@lWv&d;XEoL_7j;y2WwwzmBAjI}j zptwQiomrp;T$`qNq0~=_ydU6MY6Cs!as$_4y7!Zg%-zaQq)Ue$oz!5;vt5DpV&l={H5KQ*RhJsC9MYir-d> zHq^VKdIq5Hlr8;m^5$vV(N(Bx0;sw2ldzw=uuXNFW;ulK_B?L0m5c&dm`m;E&Oncl z3Z2^uv^kMsnwy!y6l9}^XyTL`u6=wgW0p-i;4*3&l2Z@a{6g|J4gS+ytvi_uYIoHE zV#&lv5J>tWnU?i~>n)D|1B_G~$y2A{oa~fh&hDZsST+uAXW@-Z#^b3+{ z2aG~az&WiHdNmC7`gU52gNzO`*r7Wy(UelKq0_I%9QaL}0|SM}Q5nCdAHGOvGwcja zKf)2$3{CG+k|3(CP|SY5b<@q_|16)8(}1rbEk^{8h29FlH8BxSzN zZ)ce2f~+O|9)!&mT5e8{DJwMtqH`Xe?CnbaSWU>D#55M8k>E^1l?GYi=|*9!;_0tD#bGcin>MYw<)qyShslE zs#*Iyx9{HZIR`o2ZXE14_uIs4Op~eDNaZL+G0vl^I(E1Su`rn;n7W#0jMF+N(kcPO z#*m#6W70Af;>?X)5$6V&)@lP-Fgm);g^=KtjZI8p?D!_eW74M=Q}o|46HYWXV!T6H z2E8J?i?TYduUfZ zK-n9=|IqJ|Tzu+zgCRw4+`Hj#Cwf>eiwp$>_Wt<3r*pjsf3l&>5+@mjk9Im8Ahj-~ z8{lP)#~%9P1wQ3Aj8b~z!w>!Lez%;Z{oj7_rp}?tX6~9zM@Jui`tTnjU1-^rA^-q> Cn&~nC literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_c468bb2.pf_fragment b/docs/devmanual/search/fragment/en_c468bb2.pf_fragment deleted file mode 100644 index e796b3e37118c321021782232833334c88c8ae42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3150 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 diff --git a/docs/devmanual/search/fragment/en_d323b3f.pf_fragment b/docs/devmanual/search/fragment/en_d323b3f.pf_fragment deleted file mode 100644 index fc94ee2df743e3410f9fc5d30e5fa94224bce107..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2307 zcmV+e3H?Odz(c- zi?qyy5>=A2;{*ZjZ`|LzUve{iNOB(AUF_koEs{tS=fRn8z9CuBoKHo`E?CBHd$pFm zVQ(-~i^QtL3=Gd|%@bNCw5pPYu(RZHVUs!6Vk)>c>C6_g*Y9Pjw7j$+dt#MVW+uq) z$<8<>@iip}YC){}&%gd{QZjaG2`ve&1VOa~)RI>1Zs*v(NDiB?8MQ){ zM3r($wmaio6Jdy*agx_mGkBuSCqHgPUQ&y8)f9Za;-w*?B$|J%xv^k#yH%BeYicqV zM%7@)4KIn(BT!mZ~PkQf*T*nz}bCHK%MzW_0dWSXc0y5nY$1D08A> z1Yup4mKpCmDKu!VMCCl4Dk;?hXX7MYXSie+HtW?jfhU<%hLiW)V(7gmWU?eoElQ~< zLmjA5g3rcw@&)miBzaE~)p(R%Qcc!o)>dXX803(YdXi?U7#wYn_6Kp+Ky%5d;bstR z4CWiJ(v7WjJz;!4=xSR-+hwyp2tn|$aeqiI>70^ew-KYP5ao~5n1#~p?^Spp2|Bmp z=`kQKYL*H^WmT&fl8-Bag)~C&kXogr8NObBz3x%xto_hT5o!=Erep{52pFWK8K3#o zcLD~aa6f1R7#g@xm^E^KPLcz1-rMo)kl1C#hfvU!3`IJ)G^#xBxrwz8HR{BlE51ww z8$K8VMuOEi2}XWM$oJo`?Vaqekz24+(t@A&hVWG2L>GYbULW44+PT2+E6!x9pinvZ z`ZLackfHP5FXRp-Cp5e22m&~BZoQxzm?>jBb|2)%+pmCAr5i$sDX1QXe{UpT_`NnRn`#&P9$D5P;g%f?Py3pfYHY0&D}V2Euj#q|Afx zaqjjr(L=|0z;{q_fSfBF8pq`tAe+o0^JcLc4?mxgPfdVUiWEP!u|u}>KCg5&LJcgf za7%E?4+@%*zmf65Hh$WcxgyUXCce=4HAM|j%^Zs#=ZDyO{vc*qaH~buwwLF9@^WJn zb)iZlZnz6aL;UdJ;Ane?c&w)@uyCt%(H~>`SYq~k+FVKNB__i$fv|1)zT0c7_bEOa ztdS%ExX7Aua^^T>2F(rps%=$U;tL@1XR?P#_o(L%^nUT2rEd9Yscbq`HI@#bdpm+- zL7j)OO+tL!bk~{{si9RtDjcVsKFy{IJ)5s z{kd)MF|^J|0DPZhs#+$%?+ED`51s*c6v3$o^zGWtlomH0t-%E}?E>!#DukH)1q!c{ z6s%mszeulf`7GGz<|(q~mjuw7oZiYO>Zxdu*9_@~n(+{brcK7qE#p2~g zF{b1EV&l!5w@0U!TlqgW*WbUpxyWwRP5q*Nt#|hKPd2`6t{*KjIvIc6yZM-{f8F}} zVM|}0U0UjT?hbE$@Qxr&6A-vnbfe>_{f9|pCOC>lbcpn@E~;1? zZau)yXggneSUL59sXOb1lLdOL`Y$M^T*vnj;m~YSnvYl`OKGjKSn-uFU05b|4;4T z57F2#(QDt02 zs1?-Qz&Jr0T~M+l6pwKeyH=p@u+GE_>Wk{Hw{W)LcPP;{gs$G#K%O;cZBtix0z%iZ z)*xdB!yqB@HQYZve~djJ>}QvqeHEcOoZr$v#w32zUAK2OvCPXS@)ov>euF3?h4|~4 z;Do!=8RBuxlQsvE3MZh7F7Yy?;caKkQ}u9r9vK7LV6~35*{SisUw%1( z(^ro^$57Y9c+2LxiYkh_j0dm@w+jxkl!Wsyp4y!g%>@zM`r$3eMFx9(xcK{SbEDsz z3JGyDz2WU$AGd@Qo_bbT$>C=3TUUCZP!xa$n3|>oQRLsge#AF8DpX}2g)gYYTgV>R z2VRBLfipkf_JDp0e?Q5@9Op84RMTk!Nt#oWz{eVfEkp&S`#q_ABznX3yM9;R%=0LJ z^qYk`Mw!aY6G9ssPfB%f`QTuYRx2 d%%5airBB^>tOn-0jk{m&{tFEpzYD4s000JeaSH$d diff --git a/docs/devmanual/search/fragment/en_dbcf709.pf_fragment b/docs/devmanual/search/fragment/en_dbcf709.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..203db7b9cb8e3d8475dc81700b82ac38ee825103 GIT binary patch literal 435 zcmV;k0ZjfMiwFP!00002|GiSdZrd;n{S`vH#&UC5fcaJ|=+HygLv|a6LXl)!sVo{6 z`JB+oHH8ozW zgvLNN5T`v}5ZFF`-bt9B67HvR312KN!vDT*y@$?vtD*(10_-j9F$Dt{fNDEq5nApL zu)G9YjE*^eNx18nV^vx9kFuP53arcT=^;Tys}Id87t4&{Ky317h>u}p

    4=o*LQ?0PM<008-q*pmPN literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_9c36d08.pf_fragment b/docs/devmanual/search/fragment/en_ddf2b83.pf_fragment similarity index 97% rename from docs/devmanual/search/fragment/en_9c36d08.pf_fragment rename to docs/devmanual/search/fragment/en_ddf2b83.pf_fragment index 7af3e7e4c59c753fa817384e3250ac4bc8992927..dcb894a4f758e1d9b38ee8c95bac39501aaad386 100644 GIT binary patch delta 23 fcmca2enou4G$zKko2N6Ga&p`*uev{3i-7?Ec8&=L delta 23 fcmca2enou4G$zJ3o2N6Ga&o*9-=jKNi-7?EazO|^ diff --git a/docs/devmanual/search/fragment/en_e42b384.pf_fragment b/docs/devmanual/search/fragment/en_e42b384.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..43596365f2baeea857e884bed6f63841ec1670e2 GIT binary patch literal 1793 zcmV+c2mbgUiwFP!00002|J7IPZrrvKew8^oKm2DU?KW+XE4x8+MS_#mZtbF%I0(2B zwPH+(sz~j68yM(o^!55A{f3lw<+v`AKSi6swlo^f^_vS@v67`~iZ?}Gyo>ryM_H6s zW)nLT+w>RJ#!Hc3%BF~0-LF)WRIb+1EXqyeWaGen;!J03rRed=qqBs@*My##4LS4s zufN%ZzUftdskbDrT4N;%+Q_9<4o^)%)(NL_DwUQW$NH`5K@W!6jDAwB|3ZVw!4a#OcjI@PS05$x8<*3MWV#A;1TXj;NVX9iz{p7`2q=@Q8xQwEh(cBY<~ z_NNX!FTHr7;YhKiO02y%ee2Ed`lexX%$O#G%84eI!KfUZ_7eWEU!~F7Y?zywx;}Tx zH1PXOx*%?sUCK2Z{6ad;Z?J@xTPn<^(MA+Q07xUW)=fZ>17Z}vpxEpzO5Tc&4l3tb zo2BUr!S9y|0-3&i@cd~ya;2TrQdnuzQAfI7e4Z>mOXhJQ*XdN+0qbsd{viZ|g*)>z zdMnm~;>WvVqNr7KDy-d@uK2PY9w=txZdm#?U_DCKhC_v1?gR@*>nZPXaiO@0$u=Q z)XATFnrBIm7$C1HC^+`p+k)a0nU|l;h-l}#=42#Te%7P zThlq_`1I3z{OYuqfWnn@P0)n@@PZJiQgyY@fUkb7eVjh3_uVS>oX>KdPfm0D3pg!_ z2Za}sTRRFlub9WD%`%T~V`zBt%FmV1y=`TBU2vz=$c^>D%joz&m@nr)o<8w{@6+-4 z2T;Qwr<|7ezZbSPL)$-21C5TgYAyjq{pCeu-4=#aQX+1|zyZz4E0@AZP^wOrprpYh zPfH;n9h5M}S>@vXGl@(BBTG7Uo7YI z`4JV$wpwg6Dz&^iqPM+us@%qdqKtCA~sqfUv+f#DmbaTGY1Nl2tsMXXvaYI%}z8w_OJtOMAAXOpOc(gD3vWuz(*Ph1+fBx5Y$wJmnx%xP7bT9yD!9XaLytl;%{h%Pr*Q;J|={mFK7F zB3aNUNY}~#K{@Y4V@uh^Pnz8DQVzXU?|nO_u``_e&M;0veE6pg9~ipogzLlBf;jlV z`+E-S-z~?!bz;*2X?I_GY^X7Sp~Mx^&k-}dY;jX$xD4he^fjW8?FY+Py}0o5ZvG}m zG;rtrY%!m45yB}OWhkl$colex{K|n#@dsaGA=v{OXObjU_3GxO8&`9oem$F> zx9in-{^bWBnkX$yZtpm>_~g!0(`zRh+iHbi?u6WR3%w!ye_z}bzU-X?{cid3j$=@B jTjt@tkCmB+je9Tjt`+=dU=9zjUtj+LW7Q@>k`Vv^i9nh4 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_efa0481.pf_fragment b/docs/devmanual/search/fragment/en_efa0481.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..2f98982da730d552face8c0e57c185b745820a7d GIT binary patch literal 2189 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

      =- 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 pSM3b@54(2%0DGXXV zwwX|(MpAaYAi!Q@ueT@JM^cjQq`}%1*kA;qq;tM=zpxPtp3AzNmPL7)v_>V9Bwy-v z>U3&z%SCHAbxTeQT~%!@3*n@$(;2%pbDfHGCW;HL%TzQ?D)3)BZM0p=Z0RbMjFLjv zj%x?lH zd>8G&%dQ9e;DS!WLH!Axi02a6kF2zJmD5{b z;mBJ8Lk1=sGN-k&lLvqbVhJ%yA)sb0UPkjBTuDbGbtLw8Q5Vq0P9@GC_@Ftbz{bziNYtwqBmfLij_J*{SZ`g z%AfBZ`iHK2Ok-tFw|4c=>4`%MMO?$S!QkM3qE6p~FtP}>&{L6&AEH;geO5u;7NJKSg*Jy6LFoW^w_85;towqFT=bAaRH?GT^r4e<;g z4k*RIZrt+Gn2M6J?SwD6(l+0=nYyWZ(P%C2PapYWp&HhPIm9MAw{#k|4D1+w!SYJm zrEXQ(4JQox0WSrIoJ%&Oq(!-ht!S91hgtYdf{at|40!0A3+KIH{qBPqjM$-BDv3@N z8>F|ml4Yq7PQzKyX8Vj?tj5t2`$5{3&?vaJSaNYea=tSzDbz%_)OD|d8f?8h*}sIl zf5xW8bI~fG#Q?#2Zx3{Y%1DbX|9;8!lXxea_SVjw!W=N_y_w#{zw>Wr6f8&1U`q(w z0@foltO_eBwmZvbhCvp+y&pD#G%gBwd*1pzBes)nb%KD~_KTfmItz=Mi&~Y^`o`Jq zAVYXM%X~&5ljc$d_xMDD!}}F>TY*Z}2yvbVcogXOz0=+dkl)|A^V6Oy!~&CK&moM+qDXcAHEtuSQA5PT-U{=@jGg^$k~}1aQ!LsXg|G%LEsLD&FWyxq>3H z#8J=ZDOU8Auqi&acmKH%hMv6q!h4zEVL{C2hI`QLmo(~i(m7xosW@zJeZyO%9o za~BIZXaKqG?zIPgbPasgI*0eS@UVXg!8|Xet InD-9=0OFS}O#lD@ literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_f3a924a.pf_fragment b/docs/devmanual/search/fragment/en_f3a924a.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..73bf1f6d2ec9a0da225d9cd148adc5b8db33d625 GIT binary patch literal 1265 zcmV&+;c9~ic%M*Q8!Ae`>gXen`M)1 zlu6e_-9$WHMdw|p44qw_E^^vF?pI_239@3NJ-LFcDXX~F{7a%WREq{NYB(d;P;@OH z5>z{-eM41@EqT4tK4@yayW7y(n~2zC8d^wU1&KzH!V&A*Rx4U=C{~&-=NE73YO$cG zMU5R1NeVc`X{$+Cn@+)j4TzeYEHx5?=Z)(;)kY$H?N>(XKx-2#*F_pO$T}%dUox{g zvI$A}F5%KOg(*8PqCr7w(+Ic{c0O!FIr@UwUJx6j3uIg?Ef(L5s>XFbb77jSe|o#DD(ylLJ*QL<0z<*CKXak7$X) zgbg?>jN(X3ojjMF@X7$RAyV%v)r^$4hpiSZDYs6s>!;bJyl@H)VpM@58o90jc#C!l zSsi*VfT(K#RnWE=C#;*2+RnGI;u-)88kUgF=@-Tv*>P;^gi4<9Qiq6=j8&Y&4rIZ# z7r^tg@T30echgcUFD9!EAs+z0fz-Ef$CC(ib7*R z#h8G~Lnb%=9nUt{ambrBGgb6#@o|b)YbCk3aG-k(-qM)WOFoJzi`L;N&VBFHD>|}Z}o$=473_3K$&h6|V7By~7uHEWl z>D@XoH(e4ZYS_iZB9G45kbF)?P)W(096(BxQxLilmP*}dNCbK{J)?0pBz*NoK{V=k);DDolak?#UnOU{c&Q|KQa3kW>{&x{pI@Eo|E_uQrTu5^2;@NJPI zxuP(VTfOsu7Q4al9UDFxg6U-i4K8^Er{UBO^S53!fB)qmv2$S!I7VOnP?Y`h>zq#0 z)P;3UZ&1*N2IWBG)&Y+l`D#=8|2BNYqf8AwN5Eme_O;Q6L4_C-JPzhO+`-_0gM1~i zvtiE3Z5nBsAHp<9oUTRmiD6v!+V!*i@D3c{u)70b5oZ1T5HjAQxaV~!DszvJFRfQO&-U7S5-J}!MEt!||2_}+hU z^kS41#zKi9o83Q*vKoBGDg(c*KKL+zVVb7c+D<)FmpR^Ot3qzvMD-k;98FJNz5MRz zGoie{u4?bQlJ&i&x9(bovabYy0|iTl6#L%EZO?^fvB|u>75}Q@{`{ zL&~C&mE-(>z0c6;y8oh4X2rG&OK^A$Qm;f9<7J2K2yE~ItMmx~24u78!|2dHtjewZ b%V{PyrKo%lSLlL-I-rE+yZ literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/fragment/en_f6ae855.pf_fragment b/docs/devmanual/search/fragment/en_f6ae855.pf_fragment new file mode 100644 index 0000000000000000000000000000000000000000..48959afd3e0ead86e3bae18d46af9917acf20694 GIT binary patch literal 3149 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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_13565fd.pf_index b/docs/devmanual/search/index/en_13565fd.pf_index deleted file mode 100644 index cefc6c2b7fdeec83ae58235b8f288f7e9e458523..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41514 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 CZl7BK literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_31ee9fc.pf_index b/docs/devmanual/search/index/en_31ee9fc.pf_index deleted file mode 100644 index 8ca80b1ff64e4f32e8a673afa446b02712a876b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40456 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 UjCiP+uj7>e2VX5zaX9_}0DSV-&j0`b diff --git a/docs/devmanual/search/index/en_418e556.pf_index b/docs/devmanual/search/index/en_418e556.pf_index deleted file mode 100644 index 9827d3718b6968548c221149d084a8c09f997536..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46662 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 diff --git a/docs/devmanual/search/index/en_49d892f.pf_index b/docs/devmanual/search/index/en_49d892f.pf_index deleted file mode 100644 index 7564d992a123b10726ccbaf65ed7f019a63ecd8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43098 zcmV(yKZ%_47AbCu-9>!ksl8;r7P)TH>N=1JZotUcwoy4Kd2^qU&B zfPVDMg6C#!8XcY(5V~6n(`_YMlR>S-vnzXtT(`VE38}T z<=%qjK3IJ;#_}d~qcIx2o8>M|jpdQEF0Ebz>pV?O;LELra9<1WoroNY=*JNE6Oyh% z{(C6N=7F0PZeOa|_ww4}S{$sqp8*p9gYBuIQ*mF9}WKr@Q=xX zy&CpDh@Xo1I^cfbRbUtJ8n7F99oPfB0qh0d1oi=M0sDcsfdjxhz`MXf;63;)@LS=x z!EcA(0lyP|7yNGcJ@9+s_rV_le$yT1ph?%CuyFA zv0^_CnH7q?em9;QLL&1BkoOb4dAncoA zzgbhkb##+_SVqCp3d;uCV!ViGT^0+Q zl}mzfxZz%ih^dIELqt6y8h8%`;asArIL;8eZp?_x_U$HRd1?&>O*x%eXKrFpQ_K)cj~bELH($H;*Ye#VuQslo5Urn z-2;mkmIzoPVTpnzn%2L$e-dCxge56QJ*u8m Nx%jy-iQ@yHosn^sV^_JSN-c|?H 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%0D%5GG5h^FNPIL5&- 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 KATxa)3;_UK6)&#< diff --git a/docs/devmanual/search/index/en_4a479d4.pf_index b/docs/devmanual/search/index/en_4a479d4.pf_index deleted file mode 100644 index f3983f5ac7a93c86fda8d823b66f2c97d5ff6341..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40137 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 N{~wsSrd9F>0RY>^+jIZ` diff --git a/docs/devmanual/search/index/en_5660ea1.pf_index b/docs/devmanual/search/index/en_5660ea1.pf_index deleted file mode 100644 index 7b3c3e75aab554bdc9ca866dc77e9c4e4a2539f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43297 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 BF^d2I diff --git a/docs/devmanual/search/index/en_56e5732.pf_index b/docs/devmanual/search/index/en_56e5732.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..f6608279f9b4f201df8d31fe6b4f2ae2f1cfb6bf GIT binary patch literal 41499 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 diff --git a/docs/devmanual/search/index/en_68fd426.pf_index b/docs/devmanual/search/index/en_68fd426.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..164d9bed80e5d662b98f6da821db1e86880ac4df GIT binary patch literal 40128 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 z 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 E09tX&RR910 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_7053ee2.pf_index b/docs/devmanual/search/index/en_7053ee2.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..aae32f2aa7ab9a4f38159b37bdeb94f7ae20207e GIT binary patch literal 43675 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 E04c+rl>h($ literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_8365865.pf_index b/docs/devmanual/search/index/en_8365865.pf_index deleted file mode 100644 index e95465f0dc37c9da1750bf38b122b4e80edcd876..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40922 zcmV(sK<&RDiwFP!00002|F!)Ee3aMLHV${(;<~R0k(o>;WD+caP@vEvHQLfby@iHk zk_^O%Kth!}xVIFVy0_Hbg}S@DySvM`*4le0?bCDK=Y7B5|D60va{ImSy_Z~Tt!s7G zH^-Xd?TvF98yc6KYQNHo#oBw~T?3X1TXhPyKP2yMf#voY*E+Fk<7&1ZgZd6q@s@2gbz(8ZjVDwV&NSt_Xi4(Wf&*3my1 zS*l!(QsdQBwYNG*#p|}Z^?T3(@O{%0s(5YGE7Rk?x>xk_zQ_tCvC zvs6?cQKkPG&TsRCrD~Vb+x|vxh#8ieK3Q88(}-#Qj<1@5m+TZ>*yx} z-}5|B>9FO%HVn4W@SG2CJs+K9scG~8^t`WHYVQqv*s-u(1>5(qdttAFeQ(%j!?7!T zzrpW?znIVJgY7&^&0HVFkb4lCh)^4LN;0jue6jx4 zj)o;xI={t-@|nBh1D4;4!u^qzL!$%dGB{6!^DG)`{?qj>Tz|ryWXoEzgQ59CP4^(M|V*Z8$tDEUy(+k6I~K2#zc(eXy_u_N-FaT{QR3fZZ8U zC#jQQ|AT(`B^qFBgl!PEQ(^lFwx3}yhdly&9qeOa?}U8;?8n1?0_^KxUvFjcH?LPQ z)uQIBcD2CDUP({T1;=7IR>5%!9NXxC5mqh_^>j9p6?x3H~*_Y3%cM)Da*OGf%! zWKKoaX~?xhbIH|$+7i)3+r2Zm(aK5%iCjC-^~B(%XfEnw0Fn;SJ&U)>f4#F z-&o&M-_3`0*Z20c=(}}q-`E^$kI}8^0?o9-JLbpQn_BC8+Uh&Cu%I!s`pW;0SLu%R z^t8myDYi2#y6Qr@WG79V(L4pS!pN#1C9*mQ_hS*bUn0442i%XcGM%(?zfw}fENh{C>vH&()SJM3k!57v=f{zw!h{X)8Hrkq4N=)g7n3q8hDu)PS|CoE>{ ze%SM2-wOM6us>+2gXG{zR$rNU={6F(^mRMCIyz%rBE9SBGj}bFb#-=h^(-Oj#;;p0 zla`Nfr=dH5Uu~7Oh&IiuUsykz`vLJnfh+J66e< zch>j(b?j`tsjH)rHto$LCCl9EE2hoe@#gj&=0m)(G1lGC74M`e5pPUPAg9$BYwvHD zn~(&#f^-=8GXcsN{eOonTJxGuDk zR?+_3)U&WHfbC?u9;-^%PH+vgQbgMA%HlSae`S$4$4cWHouCHQ4eDnrlds;VHd;Bn zbwHh|zJQHpjNXQ@Z_~tB#b2x)j-%oD4o*8<*T8ibTra`(p*1(w`PTq7 z#yUG%Ej{aV|D@R{k8})dCy?e|&|BZS!gBGaew+*>{`VDa&d_jZtA(sCRm&R!&(ivn zi4x(9(>j?&pL0QPoaDq3Gx43@!1*WLpJ%~Lx_|WnbpNjU?g7igXF4ylvgF@L;&?mU zXV5q7>LPPs8IN)Y>^1s)umkoY*!Lif2>Xe! zKLp20E9s<=sQ%?}d=00IUh6zK?&R@qhGPL7%i#C`j<4V(6Z2p=4~J_HD|IP7*zbIU zU#;||bmK$m?>)(ipb?k>*Uj`&89dCTa2;gjux;Uolg%UB2-s@aY;pWX#zj=cXnykf zg>-%a%~1Kyb7+FfM|p~0-LVH8G_1}!xUUC&Koyy%qLzV80QLo5|XO<5l|MYn@MvjHjsUW4OMg*?dwE zg~jw3KSmJDA%Xn>9KXZyC!97E-cQGsUxvtBL{31(bEx=+Chv-{%;pX{_GG@`DOxbo zBgbVzH`MobvvCkb@nl*^yeUS4%^~aJ7@CJxU!LCD*BNW*i8YEsO-NxHl>%9&g(S84 zsJ8BAOJ8l0Ua&N>?Xssm-qO*&xW0>&xvWChTUL+k=q5?qPM2m2p_j$9+?&_RoB#8O zcze7jam01Bf~+omgfUX;=ZYrL`!~>1vpWB_zvHj#pF&H}>QjH)-4&~E?1{IkK6RNcPI9)qa|LjDWpgnK8jvB_tri|l%I`*G5h5E5jg;vVoG_uHV-tmXD%jf*#cmhrjoB=pf=_(=7<`=?oL7nm> zr)6fVVFVS;QirMrHBWVrm3NvtOPxoS-PP(2^@w^(y$stX*vPW`g4}1==_8Zf9~A|$ z8TL2eXoKTaIL;9zMY`xtR&dVoa87}97MzE|*#zeRI|GfdPlIz<-B!Ogi5;v!H`BZI z!g+59PMSz;KHUf>SrOmyEL;TVX>eW%=d*CW2j^#`T%B!jT5#TGRMKlC|70lU)OWJ! zC~g*C;yY`;*}?9@_C0j{uW63So`%*qI}j0G>!h$`oAxadUs`W7UWV;Lnq<9;NQR8C z^d!@tQLjDvP-gEaOD{R|z4*B4^y%ptc4E7BF-bG_Pia98p%3dIyTvg^+a2mn?QW

      gv6)-Va+AY=C_kiCDjEK(wjn!ZHjNlJR3?9)#s#SZJ8GQqN$aEx;jKMl!59u;z)nK^y(vqJ>SD z4f6orDQweZBc^R)0JalhPk?GkE;$_riWZ9QklegCh(_7aa59 zSOCWra9jt+jc|Mg#}9D)tY`T#uskXXM4zY^$H02LXdtwwE{AoMY^&>JXFV0xjijK! z_BCvOz)=Io8$s9}Ax($IyiZ->S2Gli_fbK$T-~A7?_|<_=#$%f;C&i#p<>yE-tx0{ z<=&URo)zAvkSc|x2CkC~HT={0)&rtP}idfg+VC8`iU7y(y@!XAO#X+R?DJ zumWQ*p^b?2Ld$coj%H0t^~3T5tQ&%&>Z~%oYAG=}9hdQ&Tj7crr3GSsXkesymBx^$M_T_@?yE6YiH&q@0J zE;F3QubS5`YrbJEzG>c)RAF_8)v)QX^pZf~PPM4dNT$#Rsh*Ohu|F(JVfmaibKX0& zCTY)uy^chM+-I?=qFWL~zK|U1;rV6zhF0lPKAsNCFKkQMuF`sjI+i{b-6>w}rjF)4 zt1Hqxm}7XQGrO|Tve4uKz23w1DubpRKgQ9*Uzjd_hCVU0cA|uSxpX?)ukc{c8{X zYZ?!R?Jd}c!9HAexF*of=g_0IJI{`u!CWIFz513;LH&|H{_Ca`dlU{VR_;V3*O$*9QvpuR{H+NdGF!U76_f*cg&vswE*(*8iX%Shv;ce}ciz6bwnU*5B7NsChzBmwqQ2dH_b$>t8| z7fI%6M>1|I>8IS0ZCyKpGSB-+L}Al}Rh;4O(EIjkL8$I|ZRH=v7*zL2GNId81+Bo-a~&8#eyOK{Q4Vp0f5t zvO65hwbE_5AKuH5Fb1h#BlC0wf`*Ix{I20yRSbs(Nf&dYM`3w}hRyyfoKGO>EW^7z z2wxI>rF7Egqcc9Y5zp5R%F6Ly2mc!=Ni@<{)++~D%VX6>^|X49kS5xp^<1R=n`WF? z#(%>75KRy6WVc#N0)+onrXJ8zgLFF<2((uu(MZ#S?T7PA)WN<#BM*~kWJqy!p9l9_ z)Dia}EqGqMf3T^MYR;`!XtndJdp?5aGk9V)m(S=j2dlZUtGBN^9G<_}oR;>(>Au`M zxiQ^SWZob0IU=mK52w#zJ4qumM>PPB*0mQY02m_g*E)b~acv$X$J(VyZIFjKkf^#OE&%*g7 zTzkVc6|N4rR>5^CT#vx@3EVEY4}!Y~o>$?$9^U6jZ$aD=#Jz&JFA@JP65^3C8wn94 z+=#>qB))^BS|p7`QZtgyN74mI`WnejBo9OKa3n{Oat>17Map|ftwZWmq%KD4-ALOL zY0XGG7HP*JeG<|SMfzb#C(P^*r2mPm!;tk6e3|f7z_%ZK*TcsKnCk$z=EJoCuB+j? z9j>S0dL3B}$od?n?-2M}Xk1*g;aZB+Ey$XMtoI1{^9PV*r{%c|oO9q@$L74c zLAH59x;}vQPS_e?`w8}GurFk>V}A;c6gW<%DFvq!&RgO95H5lP6W}J$?@N9(V40z6=>B~myLRgQ5 zm2NkF5XG>2NEP;rnRIsmb|>s9u-^syqd_v?mcsrGJ-SUTgY7GMbFw|Isi!6LIqVnl zwoUWCn3pc;tj~qux)rX^N%vaKJ-SW?tOK_B0@o0b_9ARw5@KR&rG-d3gL*{04C`(* z*JT$UfcGsjn5&Nay)=eM6dMD%e``6XC)(N29PR845A?<$aJzHI zTlRECJ9`%C-I2z@7|8qE+3-K!=dSJU6Yi$7tr0>9wCJOerqS)(?tFK+)iB&5Q+R<= zJ675BcahIw3>5CrTwcI@W~75qjAj-=Zt3O0#z4^yjdeHwrS@k(Hfz_|@{AhPmdn4_!9y0YDFObQyluSC37Pl}! z>^zNB_3p@Ep;nZ#M<9DVvZtePHx$l5VS`qb9@bAu{Mhh{X#GO#d>Q`p_GQ!_>SzL<0bB6QNUW2w#!PT>t;wKm^W zm$u&stfS$O)>kuZ?jUTkZiyDAwxb9_*Txx{Qd(bP8+C#RSTb8Da*w-Ud5=I3JzZ1< z+)G(86PFD;J?K+Rmp(rO%UfDYgEd2|Y3ddAA#7wI9Kd&efIoDkdG2CuR9Oa5R<8%$ z=8M7O%g9~HZP?7x$|ak1-I2DoaQ9MePgwVe$?j8Qn)@i8^C7qk%-gv4V(m*VYV2%m zUof;Ns0H6`ex#9fb+J+DrQ3VEqkT>7;hx#iD7UPY=T`7E>(X{zMAjo`~BY2|pltCDQIi*71CUh+uB1Vl0 z^$J9Ff}nNFVY>{rD`C4qd@8rY_8NJeVEaJaDqjeUmBJsN(bEEfv?hxZG9C5<7&fx^ z!+t#MCkp&VTl5v=;e-7~*l&aVRoJ(RN904;KZhfpWi2~JAmG;oICh6)DjfU3u|FK$ zf`XAp@B}!P3Vw6Bz+pFtyWAy`auXaPAG~G&uKz za~_<1a2^Zi@o+AI^I|wR!Fes5*TZ=WoG-)qI-G=Vz7OZ8aCzZMgewECEVy#v8V}b* zxb_keO4~vgT)l7|1J`kIErzEYo;G-{gy(8_u7!6j;_4CCjkuc;cPrvSwBtC)U8YI^vc|4LQA$b9kFGunhNd5}R-yr1}q@0M9rAS$Ylsk}mJksVO ztqW;=NLz@s>yh>j(o2wDiu7SfzY*!TBK=OJ--qU5|_0g~-A;d>Om_mJC%+__4fBrYd6Hxe&F;wB_sgT(8RbSRQ;;SZAhE0X^} z%Hc?9K}s7^9!1KlNZWw4vygTH((XgrUr4V+W-saN>`XpXouQtVZVhcX{Tzdf*y>>v_0dBC8*6($+V@y#?;a5q~D)uSfh%h$j=~C&d4X zr0DM9sZlphmj8l+t8Zs_M#udnHMrJEA`;d7Ivi3&SKFB&8S&dLWs9dNJR5kE6@Hy}m z@Pport~ZhfkMyIkoaM`H z9GTZ5s{&agk#z~Ot|UtoS>HjWfiy)`22}+$3IvX;6M!OM2C}a}{st5bpx{XqwxDnh z3OAwfD-@4J@l)_`hW{Ouq#4OZ5h0!#D;T+e#YVDPyc${J?8!DVNtLkpc+_4uvSjli z1?E(Z&8bIXxGljG*=&?77u=a#T4M;PTNY9$@ySwgWR&T>UC}O^zfvQ)|n(OUm&`-oV-VC%Cu+mVQB)mrKaO zYFO68vVp(^SSQ2UF98x~!g>*`SHXIX7;7)Hhu+~Nua)c)q-qqxaVMPnZ~%*|0Isoc zy#m+!99`pf!yN~AB01LK_Q9PC_gZ+W;aLRFZP#LEz-#LiMB~_SmV%{dJ_8qn2qfNj!_>w5wT7R_xvU_dHfd zt>eW9b1J`zj2>;`(wbalG*n|m{c%a~%FzTtaW9TnO9(4@k+dJOpDi9bat41KD|sj; z@GM%TwyO7GvCDPTU+V0{b%p9FI`x$R`&#I2n`7eYDwCiKG6e`yoy6+W1jVq{>BR!h z&=iCvUz{vQ$;Q7Cw(nrK$!a}T(1Z`+xSfZb9CyxAIBApf!9xnhk)(4;7|Ru~TqRzz zA7Qn}w5EMb44lCw!NazY*F#=<7<-H?kCXF*P*MwfFQVitI!>KHaN_F>4_O{#-M^3E z@C99qG}cce%bHqWPw#1@-jMGwc{il2K-vJ(@8OlJP75J_IP%{`{>RAw0|gG0G@$%# z2DJ`ETmurVNZyFFr;xQ8zO&#v7X>$>;AX=^fG)|DgtL(F9ujEpeE`W%8Q!%aQ9lk8 z-y46n)MTW-!$LlN12P7Xxi^apwF?Qq{fm+L3bGs+F%cu02>5sjR+`UmaeN4eQ+egu zAaZU+Xe>gzA~YGHS*W_4kh3>n{T`_aNG*XH1K-8SxgMd32pxb5A1eK*x{&--Pr>>E zuY;7okm^9H8;ahy9-+}3+EzhgnS)YZgLNyc-?I9fYDH=sQWKF{3{nx**-*DY-G!V* z$k~LPYmsv^LK6_0gwTEn9f*oUQFT75E~B?pTcGZf@i_-MS0U#{3v(j{ZSDbqZu#q*l7k$@v1e;pTCW7@#*hoLzg)iL#%OkM7 z49jn1D2d;4u`H6yV0}&o?GF(qes)KW;z zfP>7IVXFgVwG&92Pbkn{?DThZ!a;lMm2holoSh)Z&tUDKak~}PUo;NP2>fnqNuNHH z{!M=1nc@n&U;MpK3zj)T0?5dIBV>6pO&a?>us;T;g%wWM#c*9i0F&4-nwX>Lv2=eq0#|N?MoFVcohpd$~)SGI)dJ137TV|o4(4%1=L*vO{g#7@r zWXJ_#N#}7KE+T5CSZwFB8EwB^uQT>-%$AkXzy^hfCoj+LFBml8qdnv@3OBSy*xG=g z^rV=4u>E&32Bz%PR4+aKXMePNZltrVp(WBCCU;Z!f|hV=V_$o3!**@>lFmks_=J3+ z`vo!BcFQiZcN%-c4dl++p(iwn?Ay;HV_^J$T=t*6YHk!?nc0CcY-3U5#`cxk(jzfZ z;4q_po-&PiTI~cEoXqX@LelF;FP@>5v%{L9=_WF5UzuSvy(Y^Sc}!--H5yRdeiG29 zmi|v+}*p_{9q)J-Xn(>{WJ3=WZ5uc0!Gj_Q7fZF;O{3g&*qv_ zxYN8vb7N0WxO@I069se5B*&q7hVH#gM+%u@@((Q_%^`Te90HPg3&?xLU5p8pA#p;2 zghYr)j@WDO#cRuTW$d-@;q~CU*qmwlsa=h|jrL~7lLo;m-(mGiV4KI}4OMV>t>K{`qY01>UKRjHcM9Vk;);r|q zK3E@;pBDq_S@j(KKe;fDrXP}CUvPAX<$E}e4$-E&mEZMRIF5?Ji;e8~rJmde`-`xD z%~9$ZIsY*w89#swn|XI*p!C0IuuO^A0uj7rl=Po^{-Qv&|HJfOY|5m`#z5Ww z_p4a$LSy!jF;MbfcNsbtTY041bZoo8d=*&&t79@j&dhCWo!hwW&pC6=cH{^inED^h zdZJyuO_5&eW<<_uyjbSb-`jRaWA7q!g(2zK*)Cc-^foQ#1mp?UX|5~t{Ltj2`Ik0_ z@#WaLonKmQHQ#xF0Mh=(ZUOH{hhwV!kZc;y1LB8~OEGx%7AN`H?57~e%42=Efp2|7Q82C{%G#$opyD@ms3 z-J+TO2+Loj#ml%P!%8mS(e>-piR$zEO>P}Jqh2!MSyo*3lf~4>{`QC-q=ra~8BOcE zjJ6*+79Qw|_L$%%+fXgNeO>J)se` zYkMpsgY51Jrm}6jS6lv)ydb$bqf9C0{ZpI7aq1?1uNuBR8IY&ws4I@R;J7LEk&c)m z`(Fpa!B!dr=GkN$B<6o%(v_--h_0p(jz z{s79KG~A4(G|(D2%d^_TAMf{&gs6@gnBz%O#Y9w|eaV-*GMdBw&8jGYg zNO~2?4oT@+j^x_~DBc4peMmV8DHlr~*jq^X7HO9v<4|NJvzbjmoi9#DHqD_eLKQ(B zgyO^Hru%lGqEW zW7LO6<|@BBn}CLRGDYKHOW~M*KA*)Q67yIYq78!P%M=mC4tzfg0c-h#!-`=mgY96FuwwU; zPw6Gt-jy(oUHpPF(j4!GZ5eDY!1fL7j*z-h@{um#=mncw;)H)Tw-_?T|6m)Hm&sKP*}^wnAua(t&SuURiN^<5=!(iJc;lW!&3*(6nJKl7+V=q7sBFz4>@8*P<;^Me25u@5OUXlCh5xl3yF}gmQL&{-*l?LBC}vQ zLOkxX#nIaLL>Sm#UJkP%BFJ5>PW^En-X zG2|P>LcWWA<<`eweMZo^H#mX8`Z3KV+OGx)CbG_i^-0)fvxIeQg5zl#p6_F+h7pN# zJBjbhzRR}+hMvwDSI!_iE1a|7Y=ZM-ILWPlGn`xDs)MT?t~F$@%VvHiC$QP#Vrt7) z(pXpe#X+(fwku$Jkfu{W-q~-eSdWp4bt)~#jWh_d2vcP&e}XkZzUrAWvuLVb&VD3& zq9mEElI(%sIf5#;SRP4w!nG1gbp)Ss5GGa)$9dvC{lm{FCA*@omME*kV5cP#fTI$Q zbKtlPj^9F(H(JDbWZC41m-sMoF_*C0@?!4 zWOxSPxeDG9y!%aOmoK?w-EYQpftfMPES=o*upUo}&`A>BMj~arBq5#;%jK|M!QYa6 z!OjC{oGIdNcHUI`?io}-V$z(wuD>e^s}BODOlDbqc3(gO3G}sXLh8yy)n`; z)WA=;9yjP{?2=;vj;x!*hh|4wS~$B*Vnb=7werE9ZX*`MZ;Dbgq{IG;zN)9MsbeUj z)Rtll2}n!5^aB{JN`4Ow&Qk5yY2(mg`){U) z%rHGuZgv>kpW&FZ*KBGt7kHW7KsfGDeoR_gTb^F#SuHNE$C8 zYGkL4;FcsI_t(m@h~#HXlgjcMO%*=>Ciy|k_z-UuPg>57SC4XNZ4#%{_x4q{Hnwy` zI$OxPA^@$iJuER%5-#(qF^5jKN82KuY|ENxLypnhS5wWAgR6K*x59n_95dl)gX3N} ztKqDN(}1gpn$^{Evq8oQT#v%_I9yM%d(HI&+*NSbz`Y0Dd&0dp+%w_cAMOL;Ziyw6 z&n3Mb?tZuzzzI;J#UM*~#5~C){_# zeIIL{?uX!hlpWyiCrPP>`#FwVWkvf{xL+qJ0{7c+zYF&_aFgqg`t~#2zrs!4{=Y=} z*x+$W9=#Wy1bC9*Nr5LFo=kYiE0hgSE~<);F&1$i+0t$ z;F$)`ba;+{=P1s~_cX&3mS~ST@U+9b7~VDTo(%74@V*G|TkyUE??*I_O$_eGRwh3XhMm(ib*!BS(F6-DZpV#UUBhw38Z&cYKjL}-rnAf{*- zV>sz?(P8*z;X(2@yX!nuj`Ir;?IM}b!F|q1B3DI~Nt_(ADZJGJ8EQB(RTwbI;;(s@i zPl&?}C$TuD?{u|JTHy>xF@E#(c22sCjtWz^agqVq=xVC^TK&crwk;A116!kaMYfoy z^6x!OUxED-6BM~ccDr?o{XW#k6!u23iF1^hf56%EkHg+*?sfH?Wk${Elg{E1-b>v@ z(|coTQ7C^ih}*KOnu?6`?Ehs~A(S?>IWNURfjK@A0z*L&!dMoV9$QwM~H5O=ZB%71DB z&XXc;9O9b!cvIip#k?X4B{ycGQK+6`g@^t;j;2Kir_~p7r0g=-FQEr@G>VoZn0BG8 z4mr`<*V)*{AihfjGcA4EK_F|xR&PY;Z@MhpX$I}mjGwI!MY~CUQ5TD=%ysG5Go~%9 zX84aDP0OWbRY-7+OJFC11-vIxu@Na?t zMfhKV|8@A^g#Uf`KSGHMC0>*ypd=Y3nJ9q~=9_e{luUosFOhBuIxkL4Z5eD=BxXm=Kbh7LB__-;zOOjx$w<>jcO+zADU$ zV59i1^u2H54@!V;xOrPx_ZhTqI%m_pX-Tw1I~pUM9QMrQIEI&grNr|aMdtObC+}tu z$$uhcwq$w5Z$|uYNZu96JXLYH-T>lz5x)%agNVNZ@pmAYil86CIt2G3ZEBe&kRi*z z4K{MW1vLYTnxfu}^>Bq5v`v8KUOEv=*oBfwuIfD8S_WK0k^}LwCMt?oelwZtYE#WT zMT3xPni;(^Poyc0NkrZxEHF29hv)B({b|R#C@=W49^sH^V=p1X-U7JFINa<%2q*U2xQNPeRtURm(28~uwM@Qt>WPP z42~oT+M6U?0)22S5qtR-3FG?|&O|tK;hZR0TD@=*RB@Tu#IM2m30w(q<-oNYTr(ux zk;aBUcgA>RJdVr*k@-Hd<{|4(sI}nOy9=15CP%Z>EERGZviQ#OCGpdJ{Q=~N#Yexf*-i@pothcHN)Gxqd_~L1>R}kF35}v8>M1llQvlRT6N3_3D zc0lnOxaP=_ZZ?u_%Zmgc8j!%9MSf1zqM=zB=`swfPOwPT3HeQZU4t;}{Kss`GIfqa zMk~ta$Tmy+6#J1>4tXH8S^27V$g7#^R7I_^~EB;yMLxi^=CwO9RqHM*doo4aET@S0QBq(jrK| zo|(TCiCg!BY>Bah_*rHMOZpfopl|XPWIuqya#Vg!cGzvmz6F6cRK692 z=MrY_@mvSbEy&)4?9Ir&4}l{PI0}IX|_nJXRkYFxFo~f?kf4v>rmF8R)X?ChRW+N$w(8<|s+RSs+9hx54@%tiKB_ zNu`;om=oK$zA;g(J^9;&)QP0D@}~(m=)J@=@9BZ0_LwJ3Y^tw4cGBt_x{6iFNbAsv zk>vl1oe`x}N0$i5Gt}rnV<6jXME_}QV9zZ{n(l_7)I!~^f7{K-gVxm8-at@R18qK9 zis?pY(q5?B%#pb+pc|SVa9_4&68%Y=I{oJkF4F{Xg zOUWT4K>As1L3ZnQbE1Up&^E>Yj!{0{Pl|e1XUv;uTgXedi<9*UwDObBgU-%lrDRDF z?Cg7afE0|b9!_NISSo4y5)?7naIR%NzOpC?xK4yNV(|ADjo!fbkl4#l%?9Kh@6~MBR?Fv6hRZ{x-oNEEe(U7Et<&ul! zGxdY|L)#ndTl&R}W+4N2CLQ9iv-fCE=_ZAeNevvl6w~SLy2FOtO<5SV;N(BLejNwR z)49kR!^0!n#3R*3LS>&~Wc-Q}Kg#dKC~`Ly9FN+HfCQd;SwBc#gXFW3vIPE(C@V+V zXq3GXWOoFEA=B9>kh}`XXCUQxq%1^9k2^&$BP&Ngq4KJ za5APi2IgDM#A)-wHbyvbo|4?Zy2Sz6pu0@_!MUDRBM&Qeu8^+Wo5T7@1baA$y2RYk zIX3@Nb~@PJjREl|3M~ zu-LGyU^*}Q6c4b9!o*hh(UPLg`-+gzH(tsYcQXfqbFj1+IX@F9_A8kcO16nk^cfpsB3K~`witU8=jRRBrHe5H#}NSxOU-~^7LZ(PBgry5r{~`?-(U{6XePJ z1fDc_D&W}zzDn*gIZrz!D=iytQpxUxm%QsA!k0&rb(SCMC*(9CXErL9p{f^E{Q*{l zbN)hZ0t(JU!G$Qegkyp#_C(ctsQL!AQ&Bq|wKGwB6U}=Ik~h#;AlHe4V^LOvsy9&e zDQe4Qhe}=t^)1v7$Z18+kI4B01?QmPd=y-SvPx7eK~*2BUPIM8sND;-(@?uFf7uRN zxTI@|pZqwaq>(-VwG!$i1FBnN_C7ZJ zKjThq5(iZ73P~&Zh9!waLvNxv5kve4;wxOOIYrsw__n$HtR7eck`cH+oSn3C_#Ur| zJuyuv9?7?PIH@Zd!)H3I=Owe06)p8zEa2=oGqC1L(|l%ovu4m262EAtIF%L%jpgMg zjis{^&IUMV!+9!PU3|q9*bjo!4JY+xGMrI3SHbxJXC1osfNOm$P>QtZahz1L7mv&g z4xeS0`TG(&z6SOOVE@ZRc`OIQ`50U^u|4b+b0w3yvIi}s0W+`KR!yE_Gw{vwy|{Wc zf6$=kp6nkDoCM6gUHW@0ja#A$nyPq#8KG9BNu>C1 z&Gm9z%v$NEO|wVne6+>&9vMSDjcpqZ&q-tqU5Ly|WZov(crQUULwyGv2cMHg@=dyL ziF;`TeYUzoSP;}1JS<1UHeNhl0|HCuX#hlk@4d|#{Uba2^LL<4`p2Ilp}c#!IL3na zozS!q{G2BHBu&hD!UA?0eK3;J6=MF0Ys*EpW}XQuog1@lcOz?4n?KWdxI*d)4olOP z^k#P4k*k>>YeHKB;Y=k6M>4%tn`zXnHV4rDkOpSdFSLF1ga>CDyYSjACAfR8VOcJw z*ZD#X^rvjkHHMXSu3oiBovhAMmm0PWVp;7=4vnSETgK?((L(dsBA7#@ew|m<5n$h@ zj%Su2JE4J=G=_}o)L3-^p{S1a%y>Iqy`bJ<|6D}!s>cYO>)n#<@u4Ku7cdg(;E=C| zLgbu88`p=(`3Sjr$gLwKp5ObGdpBL8vD+hb3O zP52H@lYojhMY54$ydFtc!B>jxU6A8KPC5uW$(ew`#tmKwCm$)hBxa0Tb{ zaH85Nk_vMr?2iiQ`zpCkByDh%#KT44_z}(!oQJ}BmP9_2|7#;Wmm=;+#6=JnMf_n% zn2dx2kd%QWKa$>){OmDE9zgO5NPbr=-+hoiUDV|}k@*}lUw}G}Q$u|hQXdnlkYqKy zgB3xfl7*azw68cpE#neo&PUb}P?9BkovfrUWf@HnU2r4A+4h&=Oo00kxEtV!ClQFa z#fVQr{6xezAifXrOOZGRiGLu;gQO%R?Tw^EkaQN3u0V1kl3xWXfpx$Z_%b*^u?|Tq zjCf87EfGTIg9*P`p-xj58wpD!>uwqSGih*TG1NDxAU9>g3aARU;q#s|!H;aDUr*n1l9W##>kr}jT>Q{$;l4;5%;Z;o z0-j~?Y!LRNM>!AF^B&?05MRp)`tkD+Ph)->;x9w|ZAdy6Nvn`Fh@@*d&pLB0GRZEw z8ondoyOUGoi(W#}J1F`b#iyfWBTBAB$vr4}41uW#9E?Ce0!w&Blzxa{5`u*Yl22g| z4Br>SkHGLAl=(0s4I|IT$SX1OW+Rz%A&1j^)YLRBkqPaharyvCm4(K?)Dh9&}JB1%a} zvF;7qvwV&6F}+kLiN>>OcyCmqkdBy zX=XwLxjD9}pQ5P*>8mwxl3o7E9B56F^8*eXv=Zd3Bsq#ql3QT^2#yBc_$z2J5K2ap z+NE+~iLc*8{*S^__{R0jzRF=?=W|>T$HsmxA+b@8E^=5z*_Z>zNy3MH9-QRBEjpQr z;%*TP=W_`X-6C@NJ;{tFb?`U1MoN6UW;zLqm)FnQ;c4nZb%WYMiZg3J3r(GIt3{%0m8p)X5u=ReXR`XN_gp+oFle1jk!1aBQ^AR1HaC5BGTTIyDO3|(eMF3Yo zsK0%@71m@P!ZjT5M~CiEkJ0KTPcvuccW_oS9UQ@)=%fI6e)p) z!7y^+Y97WjSV<`Vzm(5uh!#WJwX)Wi;9`>of8dy5i>kX=7>h>`voC8 zivW7Rz-gnH&XT(Roaukxom4JKHl8Z}fO)+4S<6VzSSO*3Ote2Jk)baN$-pLI0Lg)K zFJbz)4lXj+ronZzNwi3xWq0u++yU#Zq?z!n+>hkSRq8%kC0~d&&?A!iFHv;}?Tf&= zfg|_$`$&@Yf#QJZlVDO>B^Fb!xt^(HN5>%UF*@)|ja?B`7lhP#>KPt>SW7rrh5mV= z+9Xky%Q+C%-bb!Vj=ag|Z%JF?zUm;gQ1~oLI1tYAn5^*Ug|xm>(zP#>bZv_;j~*x# zE4RUsDq-N{miU&r)_555IcU@~Plq3<0)o|yUtmD{C!*oUu3nAO5uvfvo816ISz7g&%jL+U|66r4Hx*$_F zu%7+9@X(U=S1#nOA6q{QZ=EPG)&#K z8D`wYXwoo+J?kO$g5(&?l%Uk{qTrsvbrqPucrk3$rF&tI7p3DW$p?O3lI7DRj&cGV zN5QcWjzw^$!Fe+2_ba)}BWYSsrH%Mv60lr@V31+L<47G{PQvB}^@{pfShy!jSnCp4 zua~^L`#E;iRw0f?(ko}dMi}y6yusSZm?UL*-^KD>mzdVt)BFu-xgEsi49ZwxBBR}U zvUtreltkYeA^xEo9VxEMUf4Fkc0Fu=O7?L%?1zg?J%Qzv{cSiBg`39@M@Za%_rmca z9ACpZ2X>n@?M z_R9nyRQ*HPs(428yE!ELd#r?<9tHb64tC)@hp&Vgy9droIeUSl)1HFsZGl=Ygy(j6 z9*36~Ld?5CL{{MTJXF`WZw;3ineq(TXH_d{;YH9@4iLr84*xVfl2t zJ36p{GorjJnaSaw6>~!(YX-~clhFnnPi7iH$ApLEye1hE)*4vHN+kGR5{3Q(92Iah z!O_BapZhs@w<2L7lDP`)Q78jL#67A5Sq7T}XO_K+9bM92J*bBy|g{KO*sRq}+zo z&qKU{COwU$Uy+=KG!N1?N?w=r_77%nVO8jCxL<<%N5tnKF#*Xp)$fI*yOH!6k}aIH zL{7dB5ubpBzsP(UPgv%4;)(OT2+u3qy-y96Wa+ot)h#BcmG`y~x}hnR_5>7_!Qc^((S|=MIl% zu6mB9KNJa#NSKd=tdV|NK(&WN78Em$32T4r8E<7pvRi<&8)V!1UbxUma*iYP#rlkqxxvpl z5!P^k>oUmn*;O3T>q4xIR*E-g3&(CGba}d4 zXjhpSWA^Lg$4rmKP}4(BB=!0fx?l|T_E*@ih5aTzlO!)vF5BY?0@p9$WCJ-bvC9Pn z!rV^EI;q%$r9nkr6;5;@b$koG?;p&U%C!_|pZ-;}^DA1Du{&(c*ut8} zIg;nFH+zn`u+9aNB|*yIu;sM3o=5)qk`SY9Z4?hau&S7FU9W8+-9pbBd zQ?}CcMd#ThyYxMTPs8RE`jQgZ`d~XrDkl6+hLw!;+X9E~D@eo!85|NKbnsU=E8sj5 z&ZC%I$2EqjdO1oa3fDZimd3)o+RZ498Z*B)L|P^(LGkG-pz2Afn4xCT#uX)m=qfUd z?;M%I2%Ac)&(_OR?;(ufW;L4(}O$^*$q!>Jv75 zc9E5k9V;s^UN}__5bPx)BXNQhiF%Dw@GM`mDN1wC%poS5G)Xc$vL%zaR0?EN36s@W zN$S~Cd|(Gjh)%P__so-ZdV-MBu90+}v*d$bE__foN;=P-!hrBdj2VXaw8rmvW^ z!}=R+7EzGnCDJKVm<|i%n+z8fs#a=HkgICqMj?1oPZdjCkuH$Ixi#kmbQuaI8rbP3 zN3QM*c?WhUZP}SfpkYu=R7a4W_>P44A6~!7!|H7zPdCYeDUz6gF~wZ`&GwsLm`&n? zJS9Ajha^m$6TJu!-Ggd1F845YzKU+Y&hLbS!O zA^Rbx{M)1rE-O?kiPEL#m0T-t-4B=f1^3PU)2ap}O4~;^5n3_$V#e1>{VpJ-wl^jTD|(oNY`e zix)+vyFn8>h&q<$A(f+HbeBJT}E#clq;m?q05zpgB$IvO( zkxXq&_P*^-MLJufi*&?&Y_~S%?Qm9KEG^6(|3gt@b&DRZj%D)6l$iIGa1_a1l5tD@ zT-2}b*2d-`4NgS!961_|HgU}XNo>&zN`Du9VjJ;>nSQlZe*_Lx_nKi$I&)s+vokDu zZ8vw^Af2=b`8S&}8n(klnUqu874X`uJJ;FRvDyqknTz;B`hq0#U*JFQl@c`Ld4VT% zAYmR79x#$lCL`5~v=Bm1hu9iP8-~ymlG+6OF0A0C4M*rPgq|ckE*ANI9jDauPR$V- zbkzzy7-|5{MMELiLYZdY0Q*L%_j3{KSI2mqet_e9IPDVs!}DXV+3#?7v~6g3PSm41 zlvyT8{Pf77#_yhZ4#%W~n9^51V{C+%=m7NXbWp>~6oCJas+t*~pCAy% zdbA#vp7x03w3(STWqOgDF;0w#Zl`(4MCxfE<71JTc6pu*m>g=*Me8GPl_atnb|`hEYj9#vIwiA_+M3xKcIf=h@kPGh-!crtTll;SLiB@FSP|jeqv(=pXbVqvq z?j{<{&iO-xJcn`~Lr-^mw7IdJ^N^&0{znh};{U};?9ts@n8s%NL?C*JzQ*HT_Jml@90g($idMOUEc8Wi1tqMK24JBsc?(S3dtd)a4CZ`Hw) zlGM8M_|wtvM_FD3)g$VCp7=q|S#Rm%SEm*YO9}Afb zyb!(vk^378+W*#^*E{XkM&taY28Xv3w)2R3)AU$RrVndoSktCGF}6$G5sNW+z-XFZ zMGqJptxGBq%FGdiQ>8BCb-YdR{=2s@G+rxAsu^(XC0w1?2%l;?oO{B#5YDUNd>gJb zxTbI{hie5~XTtLnys3z{B0iUyZc{!Ij;~y#)+6;GWaJ}b46_ud?|~#>II`)x<)Nqv z#or)6{=NMX9K^5(Fx-nW17#m0l!?&IEL_PbYeK@+oTni*Q0LVTs9nw6M(YqSO*e$~h-PF0zp1^w*#RgLXkvm2?6b5QZeaZ9DoPB8#p5WQoJWXhkgS* zjE19Ka+;rm(;+#Gb0rb`S-9+QjpA_Nruo$-U3;ok*9j;*#3VATH+y=b`iXklOFdnz zxsj}2X`!q-xFSbtj3B6)ZdMzEJ?=)@Cr0c1S`NZAL&DSkFp|U}a|=U{DYKArC~|Bl zyB$^eMl*?_#X2MI8HOua^=;BZ=_vK3cnAnXu9Ntw0!e-!AHxApBo`=+7K^dL;Sx0n z`)k6W{SNGZ5!y@RRaS_y*+uZ|jkp7m@Dm4>rQMIL6H&HnF>;-LjPk>KBD{CQ`yS%li1Q(CN?=>z_szp4|3;b7b9yOM%?ZP z`Z-vn;w@BsKo0jl;20DfgNm)Fcn`H9vhv3vtrb~ofo=}_9C0fu*!(V-kAelL9d4xY z)I5yit8AnyNpdv{BQBfrypg$zpnT^M^}$1R?Lx+#`)F?u#Yrm$S^&UL30y zl2|>x+i}<_J?@zRT-js{vzKfhlOx&4(mk4-+x6?z&pK^Ojbg9O^jH$%%##JSkXqAL zGGGsZs|hZGROSj<|7y5X;m#ChXup{=Ye}b%?+TJ-$xG;Ip)0w|OrX8Pw84%wq9zkU zxIq&m7_n*(1Ey6Yoi{pT&2%fWjs^p=g8qLe@Qi`%p-#}Yq@(t?SAosi#O<+G+gXkz z2u9pWE&}sZq_eF*+%%gU2{iiE z^HvKQJ7V@**`%T2k9O}^HfVt4Rm?QC7~MOy+OFOY3BC7>xkKmMqA{Wh>k-DBp~G_) z=`0)xt{Ac1iS8ldi-Dnmi*|2UcBnhXvG9k{wf*_+YYz31;N9+?y+il>+I+uA?*gV6 zX=z*#;|YyPM{}&VHR!3S8(BG|&>v;qR&u_E3~)*FYT=C7!E&A0^b#o!B=qO;ou{(% z*nTIe4UY?V;_Kw~{ZReH4%OvC;8|NX|rZHgCAeB}fh;xg5#W zNFIgcal*ni1D;HTs$SOrv2w7FgszcUT zWbKA5vfigkI^BWDIuuz)A*&f#vys(-tZroe2(>#@4+y2;KsEzvIR|*F^-vq3&f@4v zbqUlJP}f4;1a%v;A*=hL9)WrizDDF7%IZQ9P&5`rGdK(^_zQ+NWB6RsbTBAGl6wj9~9$rJq~&(9<=Zr2DDse#*CsZ}#ml2#i8pX`$C zg5x;Bz;-KaPr&vH$67KzX@&DHct16~9IW}61OSjm-Ou|VSu~q9>DnkJZ!bb-DoVzo z{85aGFGj&46f8$=HEQbv$V@cVT!cYKwHq(N@*24N&VJM#f5sT^D+!ucT-+;gv5i z_PDo^IhaiwT7~{85`WA}+Oc;fm1iUu1HVVzD|H1Xv%7@_#!KXZ4ym8jpRnx7t`b|K zS<+$_EL+TAvjH<@@<{T?{|pD`%*zhEhN}lym*_edvXd^CESzMMm2;)(;j(s0-Kc#2 zq~yo29-_(JY*)d~_5S=i^MDy!!Z($gQVWmLxNk7?R^Vex0&^?Y-ESmEt5Ex>4f=g zhUH}PiLv5Vq^?shkoA6gF^By+QpmyeIvi;dWqtyex*sRC;qH?{CmG^xT_)UkH?o6+ zU*R1%SkE0i)DQPDh})IyFH*;53qRy_oLB031fF+qB&+%L=%6$&pAHh=t&jCEuz7ca^U)c7OB3uu1 zeE=pj94_wUN*U&3B(Ci(277qWei53_JvlDK>E*a#&bpt>TmjCL#Le;+2gSMwhZ`nU ztM-KFS9o`UH(m;#)Wf?gyi?(w1#c_7o$%fY?>(|<7siUMb#VT@uaMni9Cg?aUl{cRmNoaUjv% z9N@*J>E3|lEvE9P2XF*bz0lL|FGXnfleC;YU^^4G%^V%eCB^#9RELvHNOYAbetqHI z=-h4WZ;65viEChnjy)vYshhDx{3{MygyM5hd_IbAM)7UM$le3l2chs_CYml@jN)Y| zz7WM*2(4W~ihQv!vA)ek<am{iSJ<+5Kzn5N@K~2%OYJb(WPz8}%a6 z*wlWS=4iHDY1rAat5>VYBz{nRO;+9ACT#68%N?c?m=*#@+@jkhWHqXiQnN1c2 z%ONrHrjaCAmeVfyyNTG|$vHJ-m`{-H>nk?jX{PPQT{uNuXnOT{u1wM|9c69u^UdvroK8+iJf#l}GB zf4fGXk6pLS7%2JI>x5L_Bz1~iaG^0!_CHGg-TNm9?3+@xQF8|M|YA z5YYGCgH|ugfxdR;kr~Vr8vnu?)b^rwSwIT+{bJJi7rIfHh{CZboGeWK{hG`lwI`x> zF=|(%c6|^9ucEdygu;Co9()e9bIcg2T_ucqiW!l@`HF+P7m9q|SS01WR>O9R{Cvwl z%rbgdvDA36OMJl`3F~kq^^hc zd3xyMB3ZwcECnS&C3nF31T(y{XQvRhkpfU%4BHz*g<37T$Q`71FziP_mdhB(k&z-B zx~Z|Lu|fXO8}|3?+$e1Rlj(P27uX&&1}5nX7WQ?A^(7q(%px|t z1h-v7(1&hh9-ABKY~hBQ_Ceq76k{N;^X>kR9?UTY>ZDy;b9=O}MPJj^9i0;YiECzyLWjBi+%?4he~vFGQrX zCBQ(tnKs5gQ)FQymjxN>fxcm+WvBky&ou^yNt?O#=699hq_E~~@ZKAeT<3l`uY&VV3G?{{F48%d!+RFI zm%;lgykEfk3#l&Q=Aq5RBp`Unm`6uRuC;_4IOy1XSK)vao!jW)^Sv!30ehl!Z7;`D z#}a&d$Qj3Hc{s+e;rnghlYi+(m@et7=POtW|I15}V=~-4)KGdu1M(d|ud^lEy?BW5 zg6x!#`cP*9t0wgT!G+SIv8^-O6RYD(4d`awG5Rp;x1zfq$>nwJiS$9k-A#)P=Sr!U zEfvz1O6tKy9R6bc1Z5d!s_==LtJASou8AzyS`(6WI&dy$7icET@>r>ct!A;adtus*B0wd>2CgsZ#gAJ&a5uLOfDjG`SkYGpho=9F} zBynfebx=1W=Pcx0fSk)vaR{o;K-GCBGe2P2> zn@zXFH#A9Yq*Dpw#Bs1*&-tGhNap&{%%f}hE=C^JW&-)1%b2J7C;lYNK{J@A*}Uu@ zkm8$LjGRCGoFba@8*+XRB7Yw8dqccUa1_oU?Bo+4MBGJ4B-oJ9wp3(2jqG^jK7za| z%|YfpG0InL~h3GC6z9vep9kp2b__h zeZIUIaxIq-? z(KI#Kp|o0l9+O@CVtS=>xJV0QR9xM6oS0T~a3nWEpLc z@XiaQ{1mLGNInn8@R|hOlQ>@~#QcX6%VPVJX53(LNOY02$#$z(&SV;{qZ#L_;JTj- zBrFng_%kFP_-a^hl3?kFgy-osuBylJ(!-?gFM(BaI58g*f7~n?hmyB*9+@&~CfPcc zX&i~`5_X^$nWxKj7R_(w#4Oi^r|kQ~NvPKEaCx{ewP%wV6IsM{Hlup(H;X5wXGp83 zbxd9%ITh}f-wQe#7c_;1UA#?ptGdN{NNg9A$s#RT?a*y3!MsG+$rR-Wx)ntx_X zE^i|1e`uNx%~3r;=jdh}`n1@*rzI)2eCNf|_{-&Xtbz;{h=r)uYl^nR3_Hg_k!zRi zLg(Q6V&qo&kz0xKYlA2|5utRHUmcQCMpZ&SFj4|5z7^c~5TV$=OPs@gp`jtP=T6wa z7O-g+94n;)^H*>d!FdpzE8yG$=a+C5!gU~A%O$1hO1P8Z-VD!=@TLfx-aUx72!K15 zlf@G+M&jK_dJZY2Ncj+{7Nq7Nbrez$MCv@Gd6C|Lj67tFM#dqc+g}frBTNp7z%XDk zvR4Uh*}ce_jGV)fI~#e6kvE9Emyy2*^5-J|EEFW5paey^C~8FUOcZ|&za8Y`_wNq> zJowLne=ABH2t146eh8k9;9VGYKZbiSyb@)*qwGkOJ%zFl5jqZ`%?Le;(3dFBLiuhe zZ$$YLl%I+bdyr6o%^`&Y=E~xF7IwQ77a$<-9N3?M!v;s4gs`6t$J1~U(mqnSP0oUo zFfMXR*RsnsSZug?F&@fa`v}7$8{K>)y?~^bkh%*}kJbeTlOIF!6J(*}2e_DC+UF?W zC4`K7kU1Us=c4=ql>dO5uXru4kq>u|R9L?Wo;#6x3(`J9+Na343z^f9e>VJ65okhS zu2c{|59QyX<_ja0@j)N0+A1S+Nk}nD)lNwCGJy%&IFHdMm8^X1tGJbZxTg(fj)i5k z#G)^WAqhXkgzGeR((8(qjQ2E{41F(3H1aSm-+K}513|`-+*R6J%yHhD!ZmrDz96w~ zkHjE{dNaholSJC85K~C1d+5)jRlC}v?&G4+<4!6XGN+2Q{Y3IG#C`r4wrVDKCrNj1 zNb*dsG_zi=fb(=RM>~N&HovT82cSKu6-3_-(3fOjMSe=$Lv8YGgqLQ0LI+^CSIZR5qG$C`R zW&7hhJu7y&v#+B`+~j>>xy_vU9nD>R%*CRgR8>=F9wwK0S5IW2dAPQ6^mfO|)vj+} zRaG77l%BTtRFXrwxsM#eoxR=Rd41uY-X7g%sDz&;lC18Qne&Ng0jjHL+{D7fX zuCA;f$(=Wk3+0-5rh0VU=*mj-gw{Oe#GID7RaJDjt+}hGqmgW>{&2WUPy;z#v-7EK zcdUzD*3;ZbH*5=UyQ->sr&B{$RmoMl6b289{^q^0k*OUuYINPG`cc(8G>UYZ)1r#r zp{BBSWYv!6I~oUcXDcg5R#(;6SJmxsdfSJptgIPbRaZN*Zq%qzBkStxYDVwWaOew- zsvBKjIeOcDI-{Nc(A~Pa+S-woBmdTHsKcY`=uzAC);u5U@aWOCl_M)FYwLGzuw9p{ zMpoC1uCA;bT|2tIZuF?pe{UM=&B&_S`jIuYwL>ksqEP~{Xr*vFYEE`BS&A(^JHAEr z$dMyQSN`8u4ZYO=dO_?hN7mO=kNO|lZ}-yG^|h5W{xvnD>+46=(IEW~7Y}`uk#(c% zD*yEYTJv1IwB?@%gTDOey4w2x{qp~LRCc~FHab!%eomadlN?{l7f^zelTPR1Lj#b>;td?f*7Z|6h*Ne+*Lf$eQ|*qeyOy zs;VDVTQ%x`y+Q0HtEwxjYyRF{&F>y=AzitrMMH+-OLmZdG!*sqHMFSsBh>$WVg1wPu@|bYt*)!C-RXsBxElZA?MIWSq5-J= zm&X70%0mr?-gDH*n$eYi8^)n_JACJ=y85cxYMNT3>bbA~dP%I0wbXFb&VB6aVfO5Q zc%_=!(KP1U?+QD&A9^eL@FPdnRaMvjYtx}=T~$SQtgGH`%iFo>_TO(5Z8ufb^q=ZV z+F0uA|KlRF*Y!0u^d&0)yJcf5X&Y_L|MW$=dZO(uZA>S=qnxB3SJA%oFRiz2+5bnw z*f`LD(cWBJRaO1Z?Y18TT3NJflIW@AFJ3dU_TMgveYhIl5&r(&wl&-S{i|ubs%Y2U zxphw;`BdgdyZ>pa(-uzO`#-PPrW4RatsOb~zwbXZJ^qI~Z#Og}N7H)wKN_2TuA>qE zKRU7V$n11UFDat!J8ML>wY62%qqm#*(rSARZq(>e)z#ItJ2n_v?xRPKuCA-EuiNST zP`9h=MpxC>>fM8sV$$uK+8OR`*`|F~4xQR|tiF2aU`r$2wsSjWwQAcb`XArzpY9bq zr&aHv!=2&&e`?*$l>bfb+v&vBBZp41{SxV9!svgxly&8nhW^F~$Ioup&226IcCL1m z{F}_G*^!>!Xj^w<2a7wd>S1y&v#sVAyk)^QKLkOwme={r{ajjNplI7!9Xl|)v3EhV zuY=4ghL8HYg<*qlmAT4bBW}SCw-YniY9*MpwXc^Wf;GC^YBbH~6vg37xk7KTnN-z0 zzb(QkqGVVfPJcui*BTDCl51hRL_<>}EIMx{c|Q$ayXTuO{g-3aiI99Q0Rs1P_SlhxubZ^9{e%rr zFTJ{;h9oXrt||S6FX2gM7_>!A#xu*a%taQ2{hE+uUu~8mpc_vJ8@0d<^Orq+><^Bi@lx<*~E?obb@r_?j* zRbfM22g|bA-P1!y@1RdbN15NnJaW6y%|{|F95F!Nv+n48A%NFs zuhDnWiqg>0N65^lqcktuJBAKkt8Xo#Ci=)00`VfN6Rf z!aUg)G7}8By0gi9ok!Di&d^M3n$It$d0d<3cP!CUPu;y^d2N$e*AO@MW-}$=ExL1u zPLGu2zVQ5LvyjB=@;@_Zl0{`O>&$vc)S$hO9^qJ`sWes*-b^SaIpmoFw>=2gZ*VuG zcrA)AL-DgHeka7mRb9`+Jr>2?DDFe?Y82m#VLlAY#jyP`{3HxN8N(mo&dovLgD57T zZw7|ntFsI@soS}R1jmmvweenXOf?3Ir59nzt=l1jpqE`!VNnCk&;jX|(WNgCgt#}{ z(baCAmEIb2_1O*27l#O3v**x&_H}CCnT+r;begC3&h@~X<_~b% zZGfKMG@rvXmKUNZh@#;rYDZBgin>tLZ8Y{T))l+f!m?{juso+1BrIc$=KdOTnR<94 zWH`CF-cOpX`VOP1zi~&V{pSA0)+K~<7Q?rhldNxLcGIiOiD6%3I%gKpHG_GD!m9B! z{g6+mi{x`0^JiFl>y;~26{|Y+ig+9d(H<-OuJ1}s5x->A97OJyM`&pabH zabuSYNijo^NrSVC5Z8gyLy9?x#Cr$4KO-&^aW#mWDSW4M5qKY^yPz}|r6H7#Md{us zo#mJOv)S#KlQYUJ!A0h}(k`Amf@4*M)-5 z5V#kCrxAD+fsYXQ5v4Yirl7O{rIje%6{XWrdU(uF`kT4ea0TSWX4G$NANkYd9sb7T z|9Xzx|2+Jy(eBm=ZNVlm*&6N@LNF=4CWBQv+!)CJx0b91M7flXzCgycbhI&$^A8sg zOxYVY`NpKRjDP8##(@97T8DRfhJ5?dSw^pUQCE9k8)2Y5;l}QvTkB62c%J-5|I}*7 zJL`L+b-Zj?6l0J(m11U z$J0CAL}qPiHZPUV-oY`(F8u8(bqJz$vGy<(8}?=F4Xsi~((e0);h=3l8J1=)A7$H* zq;+|Ip;(dP8NM0%q|6;YfI#x@l%QQ=aZGYHHbyeJ}xjv*Vtd*O4*=@{pr4_1~i@@_} zkQ&n$OTm0Srs}*$OJcE(vhtb&(mBx#skgod=P#NEfO)daYMQ5#XJmDe8d0ojU~8{e zajKr_IE5z0q&K~QSv`lDH7cc^5n#xeTnob z{?|k%2d~x<3|h1LBF2~$b8K?y@y|`y3i(jR$g#^LmUo;^n3NhAOU(lB%Q)`W@s6Q( zTVABr7AqzQ?}UZMZ1-hCKzuOyVz#P3IZsiF5c~n#$0j#rhw9h-`f4{4>t-RkrKVmo z%VhtsjO5TZ4g&rN*7#VN&snkB8QsF?*#Y~5?4o4H+Gi#;PDqNOlW+M=*uEA9Pnrau z!SxWy4KsytG7~IXwUF}gwD0cRXjtTpnQ_%{@+<7a>DYODATSN(@5w~;FkS6jBk7cY z1fD;G)Sr;{Ymn;#XB-g{DxSBIb^_A92z??!vEnA(!$@YY{ddf|;}%BL!(oY-QMmi5 zS!y{4D@*W+tv43$@)B(6677BsN0(_XdfI)f7jrkB&>xW;#ABpev(#1{?Bp?B`tD(7 zd8-^9ryi%>S|0s1U>(jZa_%Q%MEl?CU>R6iO-d4Fs&ac8T|79ER&tbcEM6Cr*XYFl zXXw?9c0L>JY;BCR_r;RnORT zV-4wFemL(9am1tZVK^U!^Ks-pZ+Hgti;)pR<`c*|5?P-@^^7>78&<(EU+FqPF_6}$fs6T-fS~97-S!GUI?dKa;j1EAx zLban}1S+cd^pU6t>a3$ROtP7`n1g?oa%y+^|5vHq!Y_AVfNRZKi~lXn8x9h~mX+j7 z4vDAQ!Jk3O7komNgZ(v5>7FPl-8!pV61(N+4E?iXlDF-Po%6h5`wWhg{~_BOwm*VW zTjy2&v5yv_ihW^ks8`N-Rj#I}L)D2S`7TmdGLP3xW-H@fKgz-Vj)`!(;7nnb+v}Oa z!jOED)|um}HIiFZ8$ z_IpM8RtrtW<#7Dr=ky6?#k*RpRF}l%-Yu-Pm2jK`$7OK*7La&~_k|T~B5a2bC4Ecz zkw@V;6pm&%Zh+&?pc<$4R0pb7wN#zK($*xn{sxXQTqnnw6;j<~(q2poE{{bYjlm7- zLG`)l!EVuXbA{%nRziyRf@Ovy=K7Fh)hZjmiM`LGWTa40YF|Xns>f9T#YP4Z(`l$mMQy4G? zd^mJr~p!yEH|n;ytm7@!!eJxIGqxbc^VUf4hkY4?~v)lu3M7?1Gp6`Kbij zcSSpUhMt-xv;WT6Q@_`fF?N&pjJbS|9b+t2H|tYFN1GelV}*h(kMIhqH@de!tzR=1 zpJ@;8aMtY3(AhOc8=svMZ8GmkAAEqR&pOS+JKfIdG&;;PbgRDhZ6~r=tLy2IVw>Hh z&4qWm()#|+4R-2_^*1BT4Wdi6;g!L1hXji?HOSduy^`nuX1#JJDk-qY%+$1B{UEj4 z&J|+f8(>X^3->$ohJd*?8=lGQDUOZIMTY;5?W$Gp_99tl=Ci7wu;w zmIT#jAx<(Gzf4oUIE0n11)d%~`=XrAE%+xh-^w%)YDqng(b-k8Iq5u769cKwcw3@V zQKqxwdege5B)!tjdX!`F5RvH69a$P0^LISC9qF2G#6nfC6Rqhl5=CL>Jw?w-(-sg` z-R++zh@SIaJ?+TD)Gd0ro(X&MVzL-L+nw6^quM6!czUN#YCD`a?4FK!eGC!|T`HO= zi`#v-JH72veVRUDgFbD>Ur4RuSl`azS-QX&z|IZA-8*&Bal=2h+Nr0GJJ}Q(YSi1^ z*x5q{N=NK@tvnW=8w0uj)aY-|w5RB~ZayUZx7#h{b$gL9koS+R{(iqljoH#te;xV! zvQ_E{BttLjDt7;IjJe%l4j-T0Zt}_Ndh`v+{F)oPW*rZyOV4sXzVkSa(eK2k%rV?) z2%Ghc*y4%PkbZR~Cf%W%gC6>P8SYquMMoh8*bA{{z=4Krjq%Z!0u%E#$x zTqwDKjJ_3!+knJ6j#5jvBJ1uT*A9tWkHlIeKZ5jKkaZ_7k`5sjK8v?>PZ#2jL1G?9 zK&E|)tjh=lg6{^xgHZ4Q3LZnjGbqb8ylhjZwjgaJ(le3%5i*`Y<~hh(40T`-sf|dh zMtU0O{AWCh%rlwAN$tm3Lc1X?1!>ZdxcSp#> z&8w^>M$57D`g|mNNRh(jzNX1%`9TB z>Z%p*TEX?s;NSOX^C2;2oBd|St8#e*UAOBXZAN$r2@DUG3XqVl%Xv1nzb4E0zJWNeaB)mV+yMyOgTN~Y{D9JAlvbd09}U)9M@Yu$entkr##OTaoy|-O z_6b5)r|uy)#HX6$l1s;F_sDC)se7>0gXB6ew3S{*E{pGDgkP{Cj8@S z6YsD=L}nysf_Wt%dIM}{3nRpJk}JO6q*47qLl3eY9WJEXU9gd^PWE`wN?j4e_JGdy zXR-z7Em8`v%4Ade$($MlbCj?(_7p7d2%GS?$n;9#7y-w9 zCP*>LEUr>5jK^y^nb(?0XoYA^)x2d}SH_6@&eLT|)M%j``%);_37@FpW2EkSMDh4h z$gafuGbctlcenOAtTa7!PY9#(_riiv%_SAMV6(3NOYc2_$EBPDN_Sr>RKBy+N`^)z z>*6t=nq~bgHBuGhbcy8u5Y9GEc$P14&|)bib|o#%aMQ`+l@ku@2Wb-zA^#yQ7d<}H z8+TnSJhoT}Ih0w{`Z+-ww6Df4RNirBsA~aB+K{ZOkPv$DbGa@G*LQV31Luo6cf~TB z`a=Y`bG;+-;+Ys=Mc8O`!r!6%3h!?X_fFTohFn^^}@RJgb**; zg?s7Vzp?l|DYax@MaahA_)^4OkHiC!{4vrCkhPUdhnCGi+5KE-);m~9`&2|o?ZybN zUA9o$eJ<>F&kDiag~D;SLTK&|6{fq?Hlcc|7q%w|;nSzW`Si5VKAkVjPs@b->0r%$ z$HY4uk@^VI&OrJ?WK83_hglY6eFVjxl$36ytkf_|()k$PqJaeh&|YD3k)V2rmMS^A z9jnOfuZ5#NwpMNvIZjq}cZ?=%fRqyby&5=%x2m!-MhKl~MCc6Z;#&Qc%!!AjwV7z! z!@oJmc}X#x4-)9|tV~$)SejObI>Fp)>=ms3lX+3UDaiySTg{Cl&L)r1*=sAMDl@-F zhIWMAMBuJpJbvU5T!*@P)QyofNIQ{Q7Eq@J)rOEd)f67{P2$W2F*38+!o1L}X#!x$ zSErlb3K0;t06?WgH|-}0y?YyQdbtRSCMtsik8OG%5Q z=}77nT7i^!Kj&v~>9TTvub&!b_T=@+!}D8U6Qmldh2)YXJHU#qS&zgIseBozsVg|2OS zmS1g(rCsjLc`PIo-X}S+v>5IwaL){|c}x!aA_lDwT5Q6+AM)33Z=c&4?eC0b)X-)9 zLkFW>;m%0QHik7bFDA@}#!ws6$ZCqVENG9mwV7n2Nz+Z&9}gDm|2wyjvb37hKetC0 za525+T;lzoo0=))X1i8npyWT=MLK)wNLRSIcj$_v{;3nDwb&Mpwnv-Ip2vM`mQ`K# zA6IAwa`TGhUCb@%IX;0C%VI{ zG$mT0p3S3l%H+N3S97M_8w)>~5G${7la&4u2Z*)C{@V9+ag1E=9aBzS5wqFeA|Wz69A>8= zjfRs1y}VGkbFe~cwZ@9F9V!Lh3|P)Ig~yfJ?(ltpyeTO714VHtD#M6j^7$Nwdl`ev zqq#Kdc^u3m?ldl$Mp|4wp~i5`F}%XBZ-dJ%iF4z1mjmP~A14*vTvjOvx&^Lh;c0_s z9z4e&{%0h3k&uIg5%ArQ+~bkE9J%X|`z~_7MDDN1b09AndD+O@ioB1dYIqXz0m9z% ztB^kq`FkS28TlQ^??+)V0wWQafZzavs}MXJ!D~>x8>;t4^?FpFgX&GFz7aL!QL`6n z4nR!=H9e>~4mGP$vk^5Hpyn3T+>4sW#aJQ%W2DQdH44G#1gkm>ZEzjlJpMY^*WMkYFe*}g?rII3YY7`rPg+{2DPITPFfs( zO}s+Buo4;be4%0>EdMNmws}+L$Ih2OABVkH6R?mbR=Gr2kLRl8rt|LDnA5IT2hU{2 zieN7`r>ZhbD*qMBT_z=|i_4YdN@>iCNp|0^?pIH!=hf@#Lvl&|DYgf=G7eMHj+Hv! z`$=|bo79Orj!}K`L)n9{kAQtL>{DaZN0*ZGPnJQexanRnNnN;{9C;V_;E}pWxUkYC zO5kByK3ouDSFYmpsJQ3qVij_p5fazE_AAv zVaKzQT1Se;fJt@w3~Q;=PnPnWZwe>TFM=|!=PJXTq!%yT7E@twk!mje=GHw`b(xMu z+xJ|^fFOQnADm~v`3c-3xi*{P#5MJv3>4Wddkg<%P->>si^t(L;nX`A_LBt-r0wQ9 zxL$(ydWj8r0N!U1mu)7Zj#4XSk#~nr(P^BM%%%GV3ci<6lc^ZC5W~OLlDAt*GN=`7 zYsd=q&|3Q*iDv^Dno!?$Fp4&#=pl?e0VCJx=?V*>+|_K(eF95?@H#t1r)e=~k!>9( z12gG5^JF_x%@{i}+9$KYv{=5nWtOWUQ|aVvSMdzt?7sip-p zO#rh;;G{0>CoxfJaP_iyq<3yyq?ev;w0VH^MXTOu`0w8EP`IA;XEGe<2sJWHg=vB* zY@J8(2u7M`4;E>vZ;n))D;xkfk*%zCxtv7grD=(8H{k*M&G5M4c>#YYfY(Fo$@xoB z^Qzv?>^37~vF5{xHq(xq8baL-hqSRq{|V9*1jZj7GPQH3x!g zIq@T25-zC^Y08@PNg;J}tcv)rCeru-T(86RndTCfjPX6qEl2#n_SRSu<4hqVo2yra znARiBz0Nrn&b{G!&CG9P=I^WFypd}L$0eJ#@*cbw@mFaNo62+^*=wih50J8l);i*k z*D(nR^jcagu$pDc$sFN}d&69^#E;w*O@CmGgZp*T5cy1dq^DP(&G~^-NV$^_atlWZ z+)-}V+&?>N1}l2}W;{fjIkShE+J`%yCt9?zgK3bK1QFWAs=*cf^k{o{vFQZ5AeP`h z&tw$4h__+U;rG(PvUHH&zoYN16IHTfqtoAp4qz_LGeWx04bty}rRT?7rvrPo_osIBFe}&07hmsv#_(Mu4*y zSyJ61)6NawkH{`Z_BdqkgY2V_{XVk4Meb|Jt3}>X$h%PpnI@ocUlbmN!Y&kEg~Gc~ z_%4dlP*j7WsVLqD#S2h;3W~3UzY_kV;J*naKcnOi1ac5K0)Z}+9)@5QhFyfRktn18 zUW?G-2+cwHZx~S}AS|g^hG&BqQbM1U#iR?)76#Hh*T8!eyeDz!U))=W`vFPIkhBR& zKOs2*$?qU#EP(@gQZ@E&wsg4k?w_zP5@*@1!czD(T<5}_4$ozruI+h~S*#NXpqNBz zI+E^@8-0Y7=eYm{rU3f`txW2SXN)+}62$l$ZzM5X_=8Yo9|6x_T)V=XkD^;q^e6)7 zBXA=|8W_1j;$LAY6();ajT8wA(ZJ@ZLJ<3c0GsS2wS}Z2++(z38kzEi?}7C-BWt-x zr~y(Zp62*34*SI^qEo|qh@@0Yp?0QgnIK%W-@~3LMOZ7v4Sh815hfF{lM3t;C{Nc- zyIx2qk7Ei7rmsDOWGg>vUxIc&6Vks6V7-#Iwq*k5?W-^2jqENC#O~izSD?-xTPFu`mj9 z7YucudOQ~PvzWse=-?8yUR187Lhk!GZ2eNdc&%BcdaP>VP&hw7kA2u20+y+ks|11HKSCh0qd z$0yH>6XE$%V-|SMlX!-Qq>ABjus6cq71LmUgPj1!U4xNn$IP!&j_1cEDehAULb{SPvBzCT*P!=u2Siq z2RES^d#n^q^h}y?SJMdpCOj@ov$I}snk;TQ$S0dSCO@dP*?hvOwUK852aICJ47b8;%21pe%^OjuvT zY5}_#++1NC;=xU8TH~ShUW`uCw2E5slP&7vLWJPIJ24Xj9mqF`-zqXZ6~K_rIenL#TBx-XyL6~g_SGWWU*{uGT0M2 z4##;1oR7fuC)^3Fh1ZJfWRkj-W9gD*4vZ6H_&|w+nZuMjmc`-_Xc7X}6PeD2>l_wK zjhqJI(>_aVj;BQyC%`_9LwlUN!Z{7jBcuj;CR}^Lbvaz$!957~rKC#0Ga8<0@HE5o zg~(T@h}ax>tKgkXdOfd)Wok7!nyzMHUm<3~kzz)XjZPNL0sQTUkv$hxi_~45e{kR$ zUcUXbc+O>;@;s>%{x%P@)hbk~>&)T`Q*@|eCTS?Ny^_|FZU*7E(%%AGr4M15kZ~PNO}s%k0SXOq>e#gKMY@q zvcphzJVIe9s=;C|Q>d$_ijo{B&bt(NjyHjL&mfW>MbZ;o4lwxvBtI;~CEp{Nw!>eM zI#Nm}jYVJ*0+SJ#f#J(Bd=<(LLD}IbI~HXpAk>UdD_ah~a%K7qKQeYRHSuxUrcfWT z-$pD7_j0&blX)QTOJ3_F-e~`;pZ`nW{-sN-)^w3#=wWy&0f|?U0TYzenu+||M{paR zZ!klC8q9Eo#bzZv^8zy&XQFAEMfxM%;kLd;i7yd^fc6P~pYCvPW0zqho#aQ>?#Mb8 zBUWR?Ra|;vS7c2@RzI>9V#G>}AoYF1Qoni^mLKTqx4Dx&9FR~gPw3;nduI>sqjj-5 zt&1I_){wbp^&2*pj;elLg5g*xnQ$o5%f=KUNBP-P8XrO87f3yi zt3jh1`E4k2!+!?+ucE|)(5EO*L-{zAABpl4*`QUYs|$Fqx(W%|NGL#N8#0f9Zwh?B zAp1jP|AL%cJko^vo19}gGID8v;~mxk`#uf z-Nt<<*M?4ffgnQRlMb_@;#ej$JwHkjlq%+)jyoE0YY}$`;vPiGFI@U7^<<>Ii}Y2< z3M1=VARai)@SYri<90aSlkYf;EBusmoqK0FT!--GiJO_I9%V+nROciwNCJP4jp`B( zDo&;8duI{ULBK7*t-x)-?Z6h`4&YAUE`lk6dw_d^`+)m_2Y?5Ghk%EHM}S9x$AHIy zCx9n`r+}wPMFgG&J^?-jJ_9}nz5u=iz5>1mz5%HB-vQqPKL9@hKLI}jzW~1izX87k ze*k|1f5Ep4e3|fN!3X$!@MXi71D~IR_I;)B1>qY8-*EV<;j4kK7QQ<8M!{DP-)Q*8 zz&8%Q@$l^m-vsz}gKr{yyTdmbzCC0{OoMM9_@=|RFDik1Puh zd|vqC;ERVZ0lq}|lHf~*F9p6-_y~r{g)a}jeE16BD}=8IzGCMpkGvJ#E-+u7z58nZ(C?x0CB4k`CZ2p_n?_AWEHaO?09IfthFyXk@ zij?_Ct%f2S=Q?EHiR{Oa=R#g8@^Vq=L*ODNqzWC4$`4TaEvm0TH5c_R=@NG53#(1;?+L1JHANp9?6B%n3jlxy^ofyHBMs6M@vPBm7+S_58Io9>k%Aw0$0jD zm*bfH^_wz`j8n-qr>3YInU0$a6OUx@h)ea>aK;4lE72zU5$sRa52*9mqTQ5eWOB)> z_MqZIyW?TK3%2P-)>0v(pP{}bHPDQD@2_7+i-Z&g5LV|T?EWy-Oq<|UtkS!f`_17X z0R+eK65;DRl^ccB9)y)V%$cJc0C+E9*g_=ts=OHKACq8T%dFFla*-Ghq^Z?Oq$y#! z<0RNLhlRy>1}4kNblAY}xR<(EHjQ0n|9wVG)m^28{Q&|geaSa{4AwpBH<1#Ow^~%E za#DOpsc|G__ER0IPc0;)V=c*+S0(v@H09xARirCLq=9CThIO9$o_9Jc>Cg0- zFW|`IFlA>OoU7s7!chaeOR{hA$^5h?)}4-$7>z51XZ;6Q17aW@1?!po6(#>rP+@u@ z0=-LQDoj_KxG1`XOscKI=YEhxbS)EAmvpytbX8tvL+lrqZa@+>zTj`el?k7cD&JSa zL7%;Jsn}f?h@M0q!60u@Gszla=(a?S<-+5(g`!)I;F4VSYvCmWdushairnQ~vX@qJ zr}ET7aXZnM+9+dEChr&^Uj>^Zb6J~avvRF44_+*K=1bB$NuQ~sN%ssHPpl62kbS{c z2pIw7sveLXyM+;xVvE##<#f*pt5grRrx!@Ty4Tb(xtG<* z)dU>{IMt()b7l_W)EHiqw6qT(TWk=7E}bYf=dO`x`F9B{njw}5A>S8-q^{eb)L`FI zuhP`s>I9~c)L}1YOEh=A24K}_eyAY@LOBIU)pG$+HI_?94-d+O2!#X$r@j;7IBvDL zD1KzaiTTg&kv#CX2}0#*cLW)6t3^)bOJ+?rZ-%b5aGi#@TexQAG_veuuARbD#7=W< z0uO6|42KTR7h?lyx536DbsPCDwx~NvZ@!xgtB?=l0renx+#gntsK?afNE8@;c-9Ei2tm9;fZgR?`J0`9!nZFK|G=m#7jlDM9C>l78#Q! zjtlC0X0f`1jCC%n@HDN|XVr7$K6ydCOvd0=^`?4Dy{+C+@2Zd07wSv(mHL`vWyrPi zqxwz#uKrMe^2gpq-o_z_LMm(YszDL#Ig#`g_Qvflb=0{A1E()7Hc99Rex7`4F+B0x zpg%V43Eaz+qyw@kKa*(t&dt51~GF+~v5G^r`S zNs^4nRNs@o^KuD@8q5}V0D~SKOd-paAQObW^K2&SWIsonY#*0$I2pTbzL5leLUxW6 zhsw`zWJ;;?cj5RJ&IZ%e?30XRZ4(iKKV7H%9LZ=O%N`Qm_nIu9^Eqd224B<7VmTl? z*b1&EQ6fOfp)$vw716U?iXYW+eg^l$E!$Y1#Kql6$g6lY#=+45$6Pqp!m$;O&!qHe zH=Gy3`81rb!TA?l9dLESJ(_dO-Lv8DfV-QFjI|o^n9tj&wT?N4ZL|n!lc{3(&pi|F zAK@*5x64c_%O)6v47LLnvr%`xi5Xu`794L2Pu44My1JRg{CMU><;0CQNU+qi^IOeY z&Kvi^vPrGoHUmz%b9H%g3#b|BU2hPphSsuLJ-o)$A~ ziT2KpFq^5kP;3{)E~+q-gx@zhrAbduU$~q9JH!ZZfziF~%(jyB`YdVlQ9jG1nK>br zertja^&wkNy|bfzy@NSA;XdDi!V6GzAc~Gc**b*w!-!<6%R zKI0+*^QMa>A_rv>U1mkRW2#DW?aV+gHxmiadaIKC#b&%p6CWFVV9Bd=@*Nd<| z2ge2-ft$>qW1+S+b@J*8G7sl8a}o;kIXuGC(w0WYq7o0HO-QR5=zAL<=hEn^W*Mi^ z!Fhi$7WStlO0n46bA{aa`dE0*KrAkzR<55rSR&5Ocwr(p<1&IeE~A7@x5cvce_#f` z%+~I@BsbItKtMviOm&)__=RO=h4tUL2A1~V?_-wJx*8T5TnuIxNhaw1ehy-0XUJsH z6G)rA&*UEFsxkM#UI06Rj#rbcIhwRbTCK+lp~)8lc#%JNxCW{n>R{PVpA)4yPnd~w zV-7V5ZR?FupiYfVfk#Z+eu~;RR!55v@zc!^>~l@c=Ml5!3aJ(4EK2qu;rCRG2-T;x zh!ly*<}#&Oth-s?*15U_1=CJVpFBmikUUwWUAieOTV2{wpQd+{lvwWP61rBxQ}BpE~&RQ#MDr8-o`JnS?9?_Q!cds=pgLwhL-8u!aKAoetIh^-X z4zu9QKk_&C2PLZDRb9m=B=~G4L4abNN7R+HVZSfcVqRoodhr3j%d=yh8cpCsbum(J zKA_dIf6MDF*<%S7Jp$U6vm7a;E;ngrAgLrn#0MxkaBY7Rk73u-!1LvHbPsJRq1 zH=yPY)Si#p3xddf3b`L6FOKQn3$~!J7lr+(J`UBZQGF4rZ$s@7sJ%FZLVE06)Sk<7 z^*3bOk)43-Y-Cp;dw1jnkoy>NpGEE~$g?4jJoG@`aO90c-oD6BKz1~r>eb2n-p zMeSim)*$@qMEd`YWItKX4iHLvc8H_Q*wuKO1d=}>!QW4a02(0}_!{*nr(2LJ9HLD@ ztMR0D*O|du4ML}`t}(-H{xoAu*~7k+G`az{f+fb3Q`yONa33kF VgOb%#wa64z zw{Q_*^I-KNv)=t}(xE44o0H=WQX!->%H@PONF?JbWL(R08w-GAX)+sK+coUjzU3X9 zw@ga_D}j?}&Kq-d%XZnYCE=ZyWIgTea$HJx=$_H=+;4Q~!#xenn#D#38C_%a>EnGp z;TSn@mlSIiIte}BYA-N)hFY~p8(W$h+Z#KZV{K&a)LtBW_x{*%bLtpU&7`$G+HV-% z6(M+jg4e+={XgJ!li}d`ia$+&H|nlfOF~||q7m+<4(;PhjG?M1EEtz@)g98MxvIhG z#@y`+rVQ0ZIh`Q(Xm>m9b&}fWJ`YJ-kiCpCmn~*g!D%te_GMAL>zF{DtND?;<0xWUl*zC$po{lXKUGUGaS!pC8v~!ms;_d=n%on=m=9uOFRw7gg-z% z-cT&@UPp*a?MYnoAOWC4sQP5Yl(iPQ8o{W_iz`9ep)2enX}MnMue5GM6B8Eiwl= zWIih$S$W6`BC8r%7a;3OWNk*)-B2SyinE%;uo>4`eFW+`5R8hw#6Nz7xX7OsXpnS= zue54+srHnAL~^{`&ru^OEgV!(b~mbQ0nSWztml~g^eqT2BRzi%CmVAv_?4XcmHGtI zALTN$A+E=9CkqXZ|EXuEQpyoXX+mlPQt7{8WWR;%cTq-2HF;Q;pvq!~WIT+Nqq&rB z*}XadbkIm&7Lp2pU-EbToKP=<&$x~#_3q@5`cXJoc$PEimZr`+$V~LIE{sJTzQUwm zIlxr~Z6ny#93*?l)YI9|#3lPQrH6Wry|0$1q>Q9Q zot!0XCBnqP#U`JGGat@<;5^)9h`2JA?A2(h2Dgw@;2FVΝ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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_f57a6cc.pf_index b/docs/devmanual/search/index/en_f57a6cc.pf_index deleted file mode 100644 index c5dd8119ee20c41e3645968eeeb6084b61c6fdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41866 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 diff --git a/docs/devmanual/search/pagefind-entry.json b/docs/devmanual/search/pagefind-entry.json index 9d3f0718e1..da836cd07e 100644 --- a/docs/devmanual/search/pagefind-entry.json +++ b/docs/devmanual/search/pagefind-entry.json @@ -1 +1 @@ -{"version":"1.0.4","languages":{"en":{"hash":"en_eeec74b634","wasm":"en","page_count":87}}} \ No newline at end of file +{"version":"1.0.4","languages":{"en":{"hash":"en_c66a8cf98e","wasm":"en","page_count":87}}} \ No newline at end of file diff --git a/docs/devmanual/search/pagefind.en_c66a8cf98e.pf_meta b/docs/devmanual/search/pagefind.en_c66a8cf98e.pf_meta new file mode 100644 index 0000000000000000000000000000000000000000..9f0f56b86d044dc2242333f22e12c550997cb712 GIT binary patch literal 927 zcmV;Q17Q3giwFP!00002|6No&h-5_+<#o@@sw2i#g9^sN@T%(84+Rkw3F1`8&FVj?Jrk(wDPn23QI8>na^ie?&!dZ+H|#PHO8_jk^@Zlto^O>8^;n|7S0FY3;)lqaQYM80!dfk7;PCwh)?JKFJK`oNWzq_<@H&-Aq9_ zJkX(0xj-e7Ug4<43M$IITOEQexoM*1OK#MRb*ahW5uU205rUG7Ke%tO3LFTo6cN0# zTJAUI%&4GMlM8NJ04*TMgAe#)h{d`luRO$UgG1e7%ddOa@#2#f2Msg=AgNGy2$cv|@IjBBR$P zKA%bT-ufth;y)4D^ZXzN004;D B&yD~9 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/pagefind.en_eeec74b634.pf_meta b/docs/devmanual/search/pagefind.en_eeec74b634.pf_meta deleted file mode 100644 index 2260a74191d610ff674c9ed3afc61743ad43aa3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 931 zcmV;U16=$ciwFP!00002|5a2yh@?djWoCBw)D`2e78Q(zWxBii4+W7^Fc35m!=?J~ z9Pf5#@J7XKxerm$;J`#sOauinQZpk36ERR@0~Jj~(M$tTYr4J_E`0q{_3FLXy`d|$ ztrs$j+e*y=F>Nxt!F2Rj8QGY?DY<_)5cL59_^^qWH6iI?v6sk5L*O)e}~aw zM%7^UPlq+8nr$3@>c}Ys4@gt8+U5dtfqrT2j?$%v zI;3W-Bkps#L)%o~71(c5Iaq}bhgYO+@Y)v*dpG5*Q%MzqxsVDaE){v0y(?E$X(2RN zJRygDiG~8qUhPmP(_#)V|3f&U3n_8s8$t@3HNL^Yo>Vqf`z6BOH=>1Wj!YW9m8->A z?-Ax-c4$<}SR(8T<=O%Z&IY_KsutTYJDB|-4j65i2+XCh);=jqu=qs|D^-khuxG?e zADfPL5XP;l>{Wr;*Bz>$V^jeSACtD|tVTclJV_B#DsA{&%I2twV@jWiA3FGK+wh%8 z92?e{;o!p#F%=hB)A{=%L}2HFg2i7`40t?@aNCQ*0TX3wVE@j^O7E?Y!*7#!ty;~a zek_x>>Q&{8_-S%Pq&mDMlNN6<>fv7@m@{!|VHTxJw%QbZyBL*>cZNOxee%s#iVTbN zWcGw^xKCP=^Ex3MJt+`#)>*^R7t#}>GtmqmNzX>nD4zQhk&Lyp$_{^DBv@OeV1Ad} zY>ISqu(J8LZ!>A6P_x?m_e?LaT3Y%+HiNDvC39h7Dw(SDaQNxOFebg*y*xco z7yDbr$wuSKGW>6i3(@i2`JWTVg1*x5h}g)IaU@_a5}Miyr3mwfg;V6Nm|=EhYKsXI zR}x`zRzYzJr+jk+sv!+`JSe5mX2-wA;@9cdFs^wGi+837a3E}fOJ9l0goPa%Zcb+@ zYR}L24cT<5@qaW3{}NP0&%+)~U8_#Y!SPcPXo@9;c=2v*#jFg!x$hRQ@P`ozM z@{~5K&DB+=>zOijH3)qyINq{TYhIt8lIWo#p&t86F7<3%d+_+9vaO7}g;jZGyWPm& z;B0Vqth?^B$y>9!A z)|1P5ER0XmcHK|L5~6Nn-mXi2Y5lzXf^Jz!jfL^G{Nwq1J$0IYO<&H>{{v8e|L_|I F005-p%%K1P diff --git a/docs/devmanual/snippets/build.gradle b/docs/devmanual/snippets/build.gradle index 879b5349fa..665142dbd2 100644 --- a/docs/devmanual/snippets/build.gradle +++ b/docs/devmanual/snippets/build.gradle @@ -1,5 +1,5 @@ dependencies { - compile group: 'fr.maif', name: 'otoroshi_2.12', version: '16.14.0-dev' - compile group: 'fr.maif', name: 'otoroshi_2.12', version: '16.14.0-dev', classifier 'assets' + compile group: 'fr.maif', name: 'otoroshi_2.12', version: '16.15.0-dev' + compile group: 'fr.maif', name: 'otoroshi_2.12', version: '16.15.0-dev', classifier 'assets' } diff --git a/docs/devmanual/snippets/build.sbt b/docs/devmanual/snippets/build.sbt index 786937b256..3bf491d237 100644 --- a/docs/devmanual/snippets/build.sbt +++ b/docs/devmanual/snippets/build.sbt @@ -1,2 +1,2 @@ -libraryDependencies += "fr.maif" %% "otoroshi" % "16.14.0-dev" -libraryDependencies += "fr.maif" %% "otoroshi" % "16.14.0-dev" classifier "assets" \ No newline at end of file +libraryDependencies += "fr.maif" %% "otoroshi" % "16.15.0-dev" +libraryDependencies += "fr.maif" %% "otoroshi" % "16.15.0-dev" classifier "assets" \ No newline at end of file diff --git a/docs/devmanual/snippets/fetch.sh b/docs/devmanual/snippets/fetch.sh index 85bca8d5f6..c1e3307a14 100644 --- a/docs/devmanual/snippets/fetch.sh +++ b/docs/devmanual/snippets/fetch.sh @@ -1,7 +1,7 @@ // #curl -curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar' +curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar' // #curl // #wget -wget 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar' +wget 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar' // #wget diff --git a/docs/devmanual/topics/expression-language.html b/docs/devmanual/topics/expression-language.html index f01c53729c..561955a6e6 100644 --- a/docs/devmanual/topics/expression-language.html +++ b/docs/devmanual/topics/expression-language.html @@ -320,7 +320,7 @@

      Test the expression language

      You can test to get the same values than the right part by creating these following services.

      # Let's start by downloading the latest Otoroshi.
      -curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0-dev/otoroshi.jar'
      +curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'
       
       # Once downloading, run Otoroshi.
       java -Dotoroshi.adminPassword=password -jar otoroshi.jar 
      diff --git a/manual/src/main/paradox/content-pretty.json b/manual/src/main/paradox/content-pretty.json
      index e80cac2941..2d86510ed3 100644
      --- a/manual/src/main/paradox/content-pretty.json
      +++ b/manual/src/main/paradox/content-pretty.json
      @@ -53,7 +53,7 @@
           "id": "/deploy/kubernetes.md",
           "url": "/deploy/kubernetes.html",
           "title": "Kubernetes",
      -    "content": "# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.14.0\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: otoroshi-config\ntype: Opaque\nstringData:\n  oto.conf: >\n    include \"application.conf\"\n    app {\n      storage = \"redis\"\n      domain = \"apis.my.domain\"\n    }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: otoroshi-deployment\nspec:\n  selector:\n    matchLabels:\n      run: otoroshi-deployment\n  template:\n    metadata:\n      labels:\n        run: otoroshi-deployment\n    spec:\n      serviceAccountName: otoroshi-admin-user\n      terminationGracePeriodSeconds: 60\n      hostNetwork: false\n      containers:\n      - image: maif/otoroshi:16.14.0\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n        ports:\n          - containerPort: 8080\n            name: \"http\"\n            protocol: TCP\n          - containerPort: 8443\n            name: \"https\"\n            protocol: TCP\n        volumeMounts:\n        - name: otoroshi-config\n          mountPath: \"/usr/app/otoroshi/conf\"\n          readOnly: true\n      volumes:\n      - name: otoroshi-config\n        secret:\n          secretName: otoroshi-config\n        ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n:   @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n:   @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n:   @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n:   @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n:   @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n  \"KubernetesConfig\": {\n    \"enabled\": true,\n    \"endpoint\": \"https://127.0.0.1:6443\",\n    \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n    \"namespaces\": [\n      \"*\"\n    ]\n  }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n  \"KubernetesConfig\": {\n    \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n    \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n    \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n    \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n    \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n    \"namespaces\": [\"*\"], // the watched namespaces\n    \"labels\": [\"label\"], // the watched namespaces\n    \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n    \"defaultGroup\": \"default\", // the group to put services in otoroshi\n    \"ingresses\": true, // sync ingresses\n    \"crds\": false, // sync crds\n    \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n    \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n    \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n      \"service-group\": {},\n      \"service-descriptor\": {},\n      \"apikeys\": {},\n      \"global-config\": {},\n      \"jwt-verifier\": {},\n      \"tcp-service\": {},\n      \"certificate\": {},\n      \"auth-module\": {},\n      \"data-exporter\": {},\n      \"script\": {},\n      \"organization\": {},\n      \"team\": {},\n      \"data-exporter\": {},\n      \"routes\": {},\n      \"route-compositions\": {},\n      \"backends\": {}\n    }\n  }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: http-app-deployment\nspec:\n  selector:\n    matchLabels:\n      run: http-app-deployment\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        run: http-app-deployment\n    spec:\n      containers:\n      - image: kennethreitz/httpbin\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        ports:\n          - containerPort: 80\n            name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: http-app-service\nspec:\n  ports:\n    - port: 8080\n      targetPort: http\n      name: http\n  selector:\n    run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\n  annotations:\n    kubernetes.io/ingress.class: otoroshi\nspec:\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n    ...\n  }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n  name: \"otoroshi-ingress-controller\"\nspec:\n  controller: \"otoroshi.io/ingress-controller\"\n  parameters:\n    apiGroup: \"proxy.otoroshi.io/v1alpha\"\n    kind: \"IngressParameters\"\n    name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\nspec:\n  ingressClassName: otoroshi-ingress-controller\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n  \"KubernetesConfig\": {\n    ...\n    \"ingressClass\": \"*\",\n    ...\n  }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\n  annotations:\n    kubernetes.io/ingress.class: otoroshi\n    ingress.otoroshi.io/group: http-app-group\n    ingress.otoroshi.io/forceHttps: 'true'\n    ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n    ingress.otoroshi.io/overrideHost: 'true'\n    ingress.otoroshi.io/allowHttp10: 'false'\n    ingress.otoroshi.io/publicPatterns: ''\nspec:\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n  -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n  -H 'Accept: application/json' -k \\\n  https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n:   @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n:   @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n:   @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n:   @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n:   @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n:   @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n:   @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n:   @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n  \"KubernetesConfig\": {\n    \"enabled\": true,\n    \"crds\": true,\n    \"endpoint\": \"https://127.0.0.1:6443\",\n    \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n    \"namespaces\": [\n      \"*\"\n    ]\n  }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n  \"KubernetesConfig\": {\n    \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n    \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n    \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n    \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n    \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n    \"namespaces\": [\"*\"], // the watched namespaces\n    \"labels\": [\"label\"], // the watched namespaces\n    \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n    \"defaultGroup\": \"default\", // the group to put services in otoroshi\n    \"ingresses\": false, // sync ingresses\n    \"crds\": true, // sync crds\n    \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n    \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n    \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n      \"service-group\": {},\n      \"service-descriptor\": {},\n      \"apikeys\": {},\n      \"global-config\": {},\n      \"jwt-verifier\": {},\n      \"tcp-service\": {},\n      \"certificate\": {},\n      \"auth-module\": {},\n      \"data-exporter\": {},\n      \"script\": {},\n      \"organization\": {},\n      \"team\": {},\n      \"data-exporter\": {}\n    }\n  }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n  issuer: CN=Otoroshi Root\n  hosts: \n  - httpapp.foo.bar\n  - httpapps.foo.bar\n  key:\n    algo: rsa\n    size: 2048\n  subject: UID=httpapp-front, O=OtoroshiApps\n  client: false\n  ca: false\n  duration: 31536000000\n  signatureAlg: SHA256WithRSAEncryption\n  digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n  clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: certificate-1\ntype: kubernetes.io/tls\ndata:\n  tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n  return new Promise((success, failure) => {\n    const options = { \n      // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n      hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh',  \n      port: 433, \n      path: '/', \n      method: 'GET',\n      headers: {\n        'Accept': 'application/json',\n        'Otoroshi-Client-Id': clientId,\n        'Otoroshi-Client-Secret': clientSecret,\n      },\n      cert: clientCert,\n      key: clientKey,\n      ca: clientCa\n    }; \n    let data = '';\n    const req = https.request(options, (res) => { \n      res.on('data', (d) => { \n        data = data + d.toString('utf8');\n      }); \n      res.on('end', () => { \n        success({ body: JSON.parse(data), res });\n      }); \n      res.on('error', (e) => { \n        failure(e);\n      }); \n    }); \n    req.end();\n  })\n}\n\nconst options = { \n  key: backendKey, \n  cert: backendCert, \n  ca: backendCa, \n  // we want mtls behavior\n  requestCert: true, \n  rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n  res.writeHead(200, {'Content-Type': 'application/json'});\n  callApi2().then(resp => {\n    res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n  });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: http-app-deployment\nspec:\n  selector:\n    matchLabels:\n      run: http-app-deployment\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        run: http-app-deployment\n    spec:\n      containers:\n      - image: foo/http-app\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        ports:\n          - containerPort: 443\n            name: \"https\"\n        volumeMounts:\n        - name: apikey-volume\n          # here you will be able to read apikey from files \n          # - /var/run/secrets/kubernetes.io/apikeys/clientId\n          # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n          mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n          readOnly: true\n        volumeMounts:\n        - name: backend-cert-volume\n          # here you will be able to read app cert from files \n          # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n          # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n          mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n          readOnly: true\n        - name: client-cert-volume\n          # here you will be able to read app cert from files \n          # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n          # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n          mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n          readOnly: true\n      volumes:\n      - name: apikey-volume\n        secret:\n          # here we reference the secret name from apikey http-app-2-apikey-1\n          secretName: secret-2\n      - name: backend-cert-volume\n        secret:\n          # here we reference the secret name from cert http-app-certificate-backend\n          secretName: http-app-certificate-backend-secret\n      - name: client-cert-volume\n        secret:\n          # here we reference the secret name from cert http-app-certificate-client\n          secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: http-app-service\nspec:\n  ports:\n    - port: 8443\n      targetPort: https\n      name: https\n  selector:\n    run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n  name: http-app-group\n  annotations:\n    otoroshi.io/id: http-app-group\nspec:\n  description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n  # a secret name secret-1 will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: secret-1\n  authorizedEntities: \n  - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n  # a secret name secret-1 will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: secret-2\n  authorizedEntities: \n  - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-frontend\nspec:\n  description: certificate for the http-app on otorshi frontend\n  autoRenew: true\n  csr:\n    issuer: CN=Otoroshi Root\n    hosts: \n    - httpapp.foo.bar\n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-front, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-backend\nspec:\n  description: certificate for the http-app deployed on pods\n  autoRenew: true\n  # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: http-app-certificate-backend-secret\n  csr:\n    issuer: CN=Otoroshi Root\n    hosts: \n    - http-app-service \n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-back, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-client\nspec:\n  description: certificate for the http-app\n  autoRenew: true\n  secretName: http-app-certificate-client-secret\n  csr:\n    issuer: CN=Otoroshi Root\n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-client, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n  name: http-app-service-descriptor\nspec:\n  description: the service descriptor for the http app\n  groups: \n  - http-app-group\n  forceHttps: true\n  hosts:\n  - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n  # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n  matchingRoot: /\n  targets:\n  - url: https://http-app-service:8443\n    # alternatively, you can use serviceName and servicePort to use pods ip addresses\n    # serviceName: http-app-service\n    # servicePort: https\n    mtlsConfig:\n      # use mtls to contact the backend\n      mtls: true\n      certs: \n        # reference the DN for the client cert\n        - UID=httpapp-client, O=OtoroshiApps\n      trustedCerts: \n        # reference the DN for the CA cert \n        - CN=Otoroshi Root\n  sendOtoroshiHeadersBack: true\n  xForwardedHeaders: true\n  overrideHost: true\n  allowHttp10: false\n  publicPatterns:\n    - /health\n  additionalHeaders:\n    x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: my-awesome-service\nspec:\n  selector:\n    # run: otoroshi-deployment\n    # or in cluster mode\n    run: otoroshi-worker-deployment\n  ports:\n  - port: 8080\n    name: \"http\"\n    targetPort: \"http\"\n  - port: 8443\n    name: \"https\"\n    targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"coreDnsIntegration\": true,                // enable coredns integration for intra cluster calls\n    \"kubeSystemNamespace\": \"kube-system\",      // the namespace where coredns is deployed\n    \"corednsConfigMap\": \"coredns\",             // the name of the coredns configmap\n    \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n    \"otoroshiNamespace\": \"otoroshi\",           // the namespace where otoroshi is deployed\n    \"clusterDomain\": \"cluster.local\",          // the domain for cluster services\n    ...\n  }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n    errors\n    health\n    ready\n    kubernetes cluster.local in-addr.arpa ip6.arpa {\n        pods insecure\n        upstream\n        fallthrough in-addr.arpa ip6.arpa\n    }\n    rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n    forward . /etc/resolv.conf\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n:   @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"kubeDnsOperatorIntegration\": true,                // enable kube-dns integration for intra cluster calls\n    \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\",    // namespace where coredns is installed\n    \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\",     // name of the coredns service\n    \"kubeDnsOperatorCoreDnsPort\": 5353,               // port of the coredns service\n    ...\n  }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n:   @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"openshiftDnsOperatorIntegration\": true,                // enable openshift dns operator integration for intra cluster calls\n    \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\",    // namespace where coredns is installed\n    \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\",     // name of the coredns service\n    \"openshiftDnsOperatorCoreDnsPort\": 5353,               // port of the coredns service\n    ...\n  }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n    - operator.openshift.io\n  resources:\n    - dnses\n  verbs:\n    - get\n    - list\n    - watch\n    - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n:   @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n:   @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n  otoroshi.io/sidecar-apikey: backend-apikey\n  otoroshi.io/sidecar-backend-cert: backend-cert\n  otoroshi.io/sidecar-client-cert: oto-client-cert\n  otoroshi.io/token-secret: secret\n  otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n:   @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-2-apikey-3\nspec:\n  exportSecret: true \n  secretName: secret-3\n  daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"
      +    "content": "# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.15.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: otoroshi-config\ntype: Opaque\nstringData:\n  oto.conf: >\n    include \"application.conf\"\n    app {\n      storage = \"redis\"\n      domain = \"apis.my.domain\"\n    }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: otoroshi-deployment\nspec:\n  selector:\n    matchLabels:\n      run: otoroshi-deployment\n  template:\n    metadata:\n      labels:\n        run: otoroshi-deployment\n    spec:\n      serviceAccountName: otoroshi-admin-user\n      terminationGracePeriodSeconds: 60\n      hostNetwork: false\n      containers:\n      - image: maif/otoroshi:16.15.0-dev\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n        ports:\n          - containerPort: 8080\n            name: \"http\"\n            protocol: TCP\n          - containerPort: 8443\n            name: \"https\"\n            protocol: TCP\n        volumeMounts:\n        - name: otoroshi-config\n          mountPath: \"/usr/app/otoroshi/conf\"\n          readOnly: true\n      volumes:\n      - name: otoroshi-config\n        secret:\n          secretName: otoroshi-config\n        ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n  - name: APP_STORAGE_ROOT\n    value: otoroshi\n  - name: APP_DOMAIN\n    value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n:   @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n:   @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n:   @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n:   @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n:   @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n:   @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n:   @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n:   @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n  \"KubernetesConfig\": {\n    \"enabled\": true,\n    \"endpoint\": \"https://127.0.0.1:6443\",\n    \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n    \"namespaces\": [\n      \"*\"\n    ]\n  }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n  \"KubernetesConfig\": {\n    \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n    \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n    \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n    \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n    \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n    \"namespaces\": [\"*\"], // the watched namespaces\n    \"labels\": [\"label\"], // the watched namespaces\n    \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n    \"defaultGroup\": \"default\", // the group to put services in otoroshi\n    \"ingresses\": true, // sync ingresses\n    \"crds\": false, // sync crds\n    \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n    \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n    \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n      \"service-group\": {},\n      \"service-descriptor\": {},\n      \"apikeys\": {},\n      \"global-config\": {},\n      \"jwt-verifier\": {},\n      \"tcp-service\": {},\n      \"certificate\": {},\n      \"auth-module\": {},\n      \"data-exporter\": {},\n      \"script\": {},\n      \"organization\": {},\n      \"team\": {},\n      \"data-exporter\": {},\n      \"routes\": {},\n      \"route-compositions\": {},\n      \"backends\": {}\n    }\n  }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: http-app-deployment\nspec:\n  selector:\n    matchLabels:\n      run: http-app-deployment\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        run: http-app-deployment\n    spec:\n      containers:\n      - image: kennethreitz/httpbin\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        ports:\n          - containerPort: 80\n            name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: http-app-service\nspec:\n  ports:\n    - port: 8080\n      targetPort: http\n      name: http\n  selector:\n    run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\n  annotations:\n    kubernetes.io/ingress.class: otoroshi\nspec:\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n    ...\n  }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n  name: \"otoroshi-ingress-controller\"\nspec:\n  controller: \"otoroshi.io/ingress-controller\"\n  parameters:\n    apiGroup: \"proxy.otoroshi.io/v1alpha\"\n    kind: \"IngressParameters\"\n    name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\nspec:\n  ingressClassName: otoroshi-ingress-controller\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n  \"KubernetesConfig\": {\n    ...\n    \"ingressClass\": \"*\",\n    ...\n  }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n  name: http-app-ingress\n  annotations:\n    kubernetes.io/ingress.class: otoroshi\n    ingress.otoroshi.io/group: http-app-group\n    ingress.otoroshi.io/forceHttps: 'true'\n    ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n    ingress.otoroshi.io/overrideHost: 'true'\n    ingress.otoroshi.io/allowHttp10: 'false'\n    ingress.otoroshi.io/publicPatterns: ''\nspec:\n  tls:\n  - hosts:\n    - httpapp.foo.bar\n    secretName: http-app-cert\n  rules:\n  - host: httpapp.foo.bar\n    http:\n      paths:\n      - path: /\n        backend:\n          serviceName: http-app-service\n          servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n  -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n  -H 'Accept: application/json' -k \\\n  https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n:   @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n:   @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n:   @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n:   @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n:   @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n:   @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n:   @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n:   @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n  \"KubernetesConfig\": {\n    \"enabled\": true,\n    \"crds\": true,\n    \"endpoint\": \"https://127.0.0.1:6443\",\n    \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n    \"namespaces\": [\n      \"*\"\n    ]\n  }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n  \"KubernetesConfig\": {\n    \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n    \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n    \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n    \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n    \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n    \"namespaces\": [\"*\"], // the watched namespaces\n    \"labels\": [\"label\"], // the watched namespaces\n    \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n    \"defaultGroup\": \"default\", // the group to put services in otoroshi\n    \"ingresses\": false, // sync ingresses\n    \"crds\": true, // sync crds\n    \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n    \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n    \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n      \"service-group\": {},\n      \"service-descriptor\": {},\n      \"apikeys\": {},\n      \"global-config\": {},\n      \"jwt-verifier\": {},\n      \"tcp-service\": {},\n      \"certificate\": {},\n      \"auth-module\": {},\n      \"data-exporter\": {},\n      \"script\": {},\n      \"organization\": {},\n      \"team\": {},\n      \"data-exporter\": {}\n    }\n  }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n  issuer: CN=Otoroshi Root\n  hosts: \n  - httpapp.foo.bar\n  - httpapps.foo.bar\n  key:\n    algo: rsa\n    size: 2048\n  subject: UID=httpapp-front, O=OtoroshiApps\n  client: false\n  ca: false\n  duration: 31536000000\n  signatureAlg: SHA256WithRSAEncryption\n  digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n  clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: certificate-1\ntype: kubernetes.io/tls\ndata:\n  tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n  ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n  return new Promise((success, failure) => {\n    const options = { \n      // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n      hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh',  \n      port: 433, \n      path: '/', \n      method: 'GET',\n      headers: {\n        'Accept': 'application/json',\n        'Otoroshi-Client-Id': clientId,\n        'Otoroshi-Client-Secret': clientSecret,\n      },\n      cert: clientCert,\n      key: clientKey,\n      ca: clientCa\n    }; \n    let data = '';\n    const req = https.request(options, (res) => { \n      res.on('data', (d) => { \n        data = data + d.toString('utf8');\n      }); \n      res.on('end', () => { \n        success({ body: JSON.parse(data), res });\n      }); \n      res.on('error', (e) => { \n        failure(e);\n      }); \n    }); \n    req.end();\n  })\n}\n\nconst options = { \n  key: backendKey, \n  cert: backendCert, \n  ca: backendCa, \n  // we want mtls behavior\n  requestCert: true, \n  rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n  res.writeHead(200, {'Content-Type': 'application/json'});\n  callApi2().then(resp => {\n    res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n  });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: http-app-deployment\nspec:\n  selector:\n    matchLabels:\n      run: http-app-deployment\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        run: http-app-deployment\n    spec:\n      containers:\n      - image: foo/http-app\n        imagePullPolicy: IfNotPresent\n        name: otoroshi\n        ports:\n          - containerPort: 443\n            name: \"https\"\n        volumeMounts:\n        - name: apikey-volume\n          # here you will be able to read apikey from files \n          # - /var/run/secrets/kubernetes.io/apikeys/clientId\n          # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n          mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n          readOnly: true\n        volumeMounts:\n        - name: backend-cert-volume\n          # here you will be able to read app cert from files \n          # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n          # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n          mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n          readOnly: true\n        - name: client-cert-volume\n          # here you will be able to read app cert from files \n          # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n          # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n          mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n          readOnly: true\n      volumes:\n      - name: apikey-volume\n        secret:\n          # here we reference the secret name from apikey http-app-2-apikey-1\n          secretName: secret-2\n      - name: backend-cert-volume\n        secret:\n          # here we reference the secret name from cert http-app-certificate-backend\n          secretName: http-app-certificate-backend-secret\n      - name: client-cert-volume\n        secret:\n          # here we reference the secret name from cert http-app-certificate-client\n          secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: http-app-service\nspec:\n  ports:\n    - port: 8443\n      targetPort: https\n      name: https\n  selector:\n    run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n  name: http-app-group\n  annotations:\n    otoroshi.io/id: http-app-group\nspec:\n  description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n  # a secret name secret-1 will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: secret-1\n  authorizedEntities: \n  - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n  # a secret name secret-1 will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: secret-2\n  authorizedEntities: \n  - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-frontend\nspec:\n  description: certificate for the http-app on otorshi frontend\n  autoRenew: true\n  csr:\n    issuer: CN=Otoroshi Root\n    hosts: \n    - httpapp.foo.bar\n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-front, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-backend\nspec:\n  description: certificate for the http-app deployed on pods\n  autoRenew: true\n  # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n  exportSecret: true \n  secretName: http-app-certificate-backend-secret\n  csr:\n    issuer: CN=Otoroshi Root\n    hosts: \n    - http-app-service \n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-back, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n  name: http-app-certificate-client\nspec:\n  description: certificate for the http-app\n  autoRenew: true\n  secretName: http-app-certificate-client-secret\n  csr:\n    issuer: CN=Otoroshi Root\n    key:\n      algo: rsa\n      size: 2048\n    subject: UID=httpapp-client, O=OtoroshiApps\n    client: false\n    ca: false\n    duration: 31536000000\n    signatureAlg: SHA256WithRSAEncryption\n    digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n  name: http-app-service-descriptor\nspec:\n  description: the service descriptor for the http app\n  groups: \n  - http-app-group\n  forceHttps: true\n  hosts:\n  - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n  # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n  matchingRoot: /\n  targets:\n  - url: https://http-app-service:8443\n    # alternatively, you can use serviceName and servicePort to use pods ip addresses\n    # serviceName: http-app-service\n    # servicePort: https\n    mtlsConfig:\n      # use mtls to contact the backend\n      mtls: true\n      certs: \n        # reference the DN for the client cert\n        - UID=httpapp-client, O=OtoroshiApps\n      trustedCerts: \n        # reference the DN for the CA cert \n        - CN=Otoroshi Root\n  sendOtoroshiHeadersBack: true\n  xForwardedHeaders: true\n  overrideHost: true\n  allowHttp10: false\n  publicPatterns:\n    - /health\n  additionalHeaders:\n    x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: my-awesome-service\nspec:\n  selector:\n    # run: otoroshi-deployment\n    # or in cluster mode\n    run: otoroshi-worker-deployment\n  ports:\n  - port: 8080\n    name: \"http\"\n    targetPort: \"http\"\n  - port: 8443\n    name: \"https\"\n    targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"coreDnsIntegration\": true,                // enable coredns integration for intra cluster calls\n    \"kubeSystemNamespace\": \"kube-system\",      // the namespace where coredns is deployed\n    \"corednsConfigMap\": \"coredns\",             // the name of the coredns configmap\n    \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n    \"otoroshiNamespace\": \"otoroshi\",           // the namespace where otoroshi is deployed\n    \"clusterDomain\": \"cluster.local\",          // the domain for cluster services\n    ...\n  }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n    errors\n    health\n    ready\n    kubernetes cluster.local in-addr.arpa ip6.arpa {\n        pods insecure\n        upstream\n        fallthrough in-addr.arpa ip6.arpa\n    }\n    rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n    forward . /etc/resolv.conf\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n:   @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"kubeDnsOperatorIntegration\": true,                // enable kube-dns integration for intra cluster calls\n    \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\",    // namespace where coredns is installed\n    \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\",     // name of the coredns service\n    \"kubeDnsOperatorCoreDnsPort\": 5353,               // port of the coredns service\n    ...\n  }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n:   @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n  \"KubernetesConfig\": {\n    ...\n    \"openshiftDnsOperatorIntegration\": true,                // enable openshift dns operator integration for intra cluster calls\n    \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\",    // namespace where coredns is installed\n    \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\",     // name of the coredns service\n    \"openshiftDnsOperatorCoreDnsPort\": 5353,               // port of the coredns service\n    ...\n  }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n    - operator.openshift.io\n  resources:\n    - dnses\n  verbs:\n    - get\n    - list\n    - watch\n    - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n:   @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n:   @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n  otoroshi.io/sidecar-apikey: backend-apikey\n  otoroshi.io/sidecar-backend-cert: backend-cert\n  otoroshi.io/sidecar-client-cert: oto-client-cert\n  otoroshi.io/token-secret: secret\n  otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n:   @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n  name: http-app-2-apikey-3\nspec:\n  exportSecret: true \n  secretName: secret-3\n  daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"
         },
         {
           "name": "scaling.md",
      @@ -186,7 +186,7 @@
           "id": "/getting-started.md",
           "url": "/getting-started.html",
           "title": "Getting Started",
      -    "content": "# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
      \nRoute plugins:\nApikeys\n
      \n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
      \nRoute plugins:\nAuthentication\n
      \n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial." + "content": "# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
      \nRoute plugins:\nApikeys\n
      \n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
      \nRoute plugins:\nAuthentication\n
      \n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial." }, { "name": "communicate-with-kafka.md", @@ -242,7 +242,7 @@ "id": "/how-to-s/import-export-otoroshi-datastore.md", "url": "/how-to-s/import-export-otoroshi-datastore.html", "title": "Import and export Otoroshi datastore", - "content": "# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`" + "content": "# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`" }, { "name": "index.md", @@ -326,7 +326,7 @@ "id": "/how-to-s/setup-otoroshi-cluster.md", "url": "/how-to-s/setup-otoroshi-cluster.html", "title": "Setup an Otoroshi cluster", - "content": "# Setup an Otoroshi cluster\n\n
      \nRoute plugins:\nAdditional Headers In\n
      \n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n" + "content": "# Setup an Otoroshi cluster\n\n
      \nRoute plugins:\nAdditional Headers In\n
      \n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n" }, { "name": "tailscale-integration.md", @@ -361,7 +361,7 @@ "id": "/how-to-s/wasmo-installation.md", "url": "/how-to-s/wasmo-installation.html", "title": "Deploy your own Wasmo", - "content": "# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: admin-api-apikey-id\n* `Apikey id`: admin-api-apikey-secret\n* `Apikey secret`: http://localhost:5001\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n" + "content": "# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: http://localhost:5001\n* `Apikey id`: admin-api-apikey-id\n* `Apikey secret`: admin-api-apikey-secret\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n" }, { "name": "working-with-eureka.md", @@ -396,28 +396,28 @@ "id": "/includes/fetch-and-start.md", "url": "/includes/fetch-and-start.html", "title": "", - "content": "\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n" + "content": "\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n" }, { "name": "initialize.md", "id": "/includes/initialize.md", "url": "/includes/initialize.html", "title": "", - "content": "\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
      \nSet up an Otoroshi\n\n
      \n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n" + "content": "\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
      \nSet up an Otoroshi\n\n
      \n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n" }, { "name": "index.md", "id": "/index.md", "url": "/index.html", "title": "Otoroshi", - "content": "# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.14.0\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n" + "content": "# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.15.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n" }, { "name": "get-otoroshi.md", "id": "/install/get-otoroshi.md", "url": "/install/get-otoroshi.html", "title": "Get Otoroshi", - "content": "# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi-16.14.0.zip\nunzip ./otoroshi-16.14.0.zip\ncd otoroshi-16.14.0\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.14.0-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n" + "content": "# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi-16.15.0-dev.zip\nunzip ./otoroshi-16.15.0-dev.zip\ncd otoroshi-16.15.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.15.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n" }, { "name": "index.md", @@ -522,7 +522,7 @@ "id": "/topics/expression-language.md", "url": "/topics/expression-language.html", "title": "Expression language", - "content": "# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```" + "content": "# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```" }, { "name": "graphql-composer.md", diff --git a/manual/src/main/paradox/content.json b/manual/src/main/paradox/content.json index b4e00dea77..eb24510122 100644 --- a/manual/src/main/paradox/content.json +++ b/manual/src/main/paradox/content.json @@ -1 +1 @@ -[{"name":"about.md","id":"/about.md","url":"/about.html","title":"About Otoroshi","content":"# About Otoroshi\n\nAt the beginning of 2017, we had the need to create a new environment to be able to create new \"digital\" products very quickly in an agile fashion at @link:[MAIF](https://www.maif.fr) { open=new }. Naturally we turned to PaaS solutions and chose the excellent @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } product to run our apps. \n\nWe also chose that every feature team will have the freedom to choose its own technological stack to build its product. It was a nice move but it has also introduced some challenges in terms of homogeneity for traceability, security, logging, ... because we did not want to force library usage in the products. We could have used something like @link:[Service Mesh Pattern](http://philcalcado.com/2017/08/03/pattern_service_mesh.html) { open=new } but the deployement model of @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } prevented us to do it.\n\nThe right solution was to use a reverse proxy or some kind of API Gateway able to provide tracability, logging, security with apikeys, quotas, DNS as a service locator, etc. We needed something easy to use, with a human friendly UI, a nice API to extends its features, true hot reconfiguration, able to generate internal events for third party usage. A couple of solutions were available at that time, but not one seems to fit our needs, there was always something missing, too complicated for our needs or not playing well with @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } deployment model.\n\nAt some point, we tried to write a small prototype to explore what could be our dream reverse proxy. The design was very simple, there were some rough edges but every major feature needed was there waiting to be enhanced.\n\n**Otoroshi** was born and we decided to move ahead with our hairy monster :)\n\n## Philosophy \n\nEvery OSS product build at @link:[MAIF](https://www.maif.fr) { open=new } like the developer portal @link:[Daikoku](https://maif.github.io/daikoku) { open=new } or @link:[Izanami](https://maif.github.io/izanami) { open=new } follow a common philosophy. \n\n* the services or API provided should be **technology agnostic**.\n* **http first**: http is the right answer to the previous quote \n* **api First**: the UI is just another client of the api. \n* **secured**: the services exposed need authentication for both humans or machines \n* **event based**: the services should expose a way to get notified of what happened inside. \n"},{"name":"api.md","id":"/api.md","url":"/api.html","title":"Admin REST API","content":"# Admin REST API\n\nOtoroshi provides a fully featured REST admin API to perform almost every operation possible in the Otoroshi dashboard. The Otoroshi dashbaord is just a regular consumer of the admin API.\n\nUsing the admin API, you can do whatever you want and enhance your Otoroshi instances with a lot of features that will feet your needs.\n\n## Swagger descriptor\n\nThe Otoroshi admin API is described using OpenAPI format and is available at :\n\nhttps://maif.github.io/otoroshi/manual/code/openapi.json\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/openapi.json\n\n## Swagger documentation\n\nYou can read the OpenAPI descriptor in a more human friendly fashion using `Swagger UI`. The swagger UI documentation of the Otoroshi admin API is available at :\n\nhttps://maif.github.io/otoroshi/swagger-ui/index.html\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/swagger/ui\n\nYou can also read the swagger UI documentation of the Otoroshi admin API below :\n\n@@@ div { .swagger-frame }\n\n\n@@@\n"},{"name":"architecture.md","id":"/architecture.md","url":"/architecture.html","title":"Architecture","content":"# Architecture\n\nWhen we started the development of Otoroshi, we had several classical patterns in mind like `Service gateway`, `Service locator`, `Circuit breakers`, etc ...\n\nAt start we thought about providing a bunch of librairies that would be included in each microservice or app to perform these tasks. But the more we were thinking about it, the more it was feeling weird, unagile, etc, it also prevented us to use any technical stack we wanted to use. So we decided to change our approach to something more universal.\n\nWe chose to make Otoroshi the central part of our microservices system, something between a reverse-proxy, a service gateway and a service locator where each call to a microservice (even from another microservice) must pass through Otoroshi. There are multiple benefits to do that, each call can be logged, audited, monitored, integrated with a circuit breaker, etc without imposing libraries and technical stack. Any service is exposed through its own domain and we rely only on DNS to handle the service location part. Any access to a service is secured by default with an api key and is supervised by a circuit breaker to avoid cascading failures.\n\n@@@ div { .centered-img }\n\n@@@\n\nOtoroshi tries to embrace our @ref:[global philosophy](./about.md#philosophy) by providing a full featured REST admin api, a gorgeous admin dashboard written in @link:[React](https://reactjs.org) { open=new } that uses the api, by generating traffic events, alerts events, audit events that can be consumed by several channels. Otoroshi also supports a bunch of datastores to better match with different use cases.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"aws.md","id":"/deploy/aws.md","url":"/deploy/aws.html","title":"AWS - Elastic Beanstalk","content":"# AWS - Elastic Beanstalk\n\nNow you want to use Otoroshi on AWS. There are multiple options to deploy Otoroshi on AWS, \nfor instance :\n\n* You can deploy the @ref:[Docker image](../install/get-otoroshi.md#from-docker) on [Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n* You can create a basic [Amazon EC2](https://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/concepts.html), access it via SSH, then \ndeploy the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file) \n* Or you can use [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk)\n\nIn this section we are going to cover how to deploy Otoroshi on [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk). \n\n## AWS Elastic Beanstalk Overview\nUnlike Clever Cloud, to deploy an application on AWS Elastic Beanstalk, you don't link your app to your VCS repository, push your code and expect it to be built and run.\n\nAWS Elastic Beanstalk does only the run part. So you have to handle your own build pipeline, upload a Zip file containing your runnable, then AWS Elastic Beanstalk will take it from there. \n \nEg: for apps running on the JVM (Scala/Java/Kotlin) a Zip with the jar inside would suffice, for apps running in a Docker container, a Zip with the DockerFile would be enough. \n\n\n## Prepare your deployment target\nActually, there are 2 options to build your target. \n\nEither you create a DockerFile from this @ref:[Docker image](../install/get-otoroshi.md#from-docker), build a zip, and do all the Otoroshi custom configuration using ENVs.\n\nOr you download the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file), do all the Otoroshi custom configuration using your own otoroshi.conf, and create a DockerFile that runs the jar using your otoroshi.conf. \n\nFor the second option your DockerFile would look like this :\n\n```dockerfile\nFROM openjdk:11\nVOLUME /tmp\nEXPOSE 8080\nADD otoroshi.jar otoroshi.jar\nADD otoroshi.conf otoroshi.conf\nRUN sh -c 'touch /otoroshi.jar'\nENV JAVA_OPTS=\"\"\nENTRYPOINT [ \"sh\", \"-c\", \"java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dconfig.file=/otoroshi.conf -jar /otoroshi.jar\" ]\n``` \n \nI'd recommend the second option.\n \nNow Zip your target (Jar + Conf + DockerFile) and get ready for deployment. \n\n## Create an Otoroshi instance on AWS Elastic Beanstalk\nFirst, go to [AWS Elastic Beanstalk Console](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/welcome), don't forget to sign in and make sure that you are in the good region (eg : eu-west-3 for Paris).\n\nHit **Get started** \n\n@@@ div { .centered-img }\n\n@@@\n\nSpecify the **Application name** of your application, Otoroshi for example.\n\n@@@ div { .centered-img }\n\n@@@\n \nChoose the **Platform** of the application you want to create, in your case use Docker.\n\nFor **Application code** choose **Upload your code** then hit **Upload**.\n\n@@@ div { .centered-img }\n\n@@@\n\nBrowse the zip created in the [previous section](#prepare-your-deployment-target) from your machine. \n\nAs you can see in the image above, you can also choose an S3 location, you can imagine that at the end of your build pipeline you upload your Zip to S3, and then get it from there (I wouldn't recommend that though).\n \nWhen the upload is done, hit **Configure more options**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nRight now an AWS Elastic Beanstalk application has been created, and by default an environment named Otoroshi-env is being created as well.\n\nAWS Elastic Beanstalk can manage multiple environments of the same application, for instance environments can be (prod, preprod, expriments...). \n\nOtoroshi is a bit particular, it doesn't make much sense to have multiple environments, since Otoroshi will handle all the requests from/to backend services regardless of the environment. \n \nAs you see in the image above, we are now configuring the Otoroshi-env, the one and only environment of Otoroshi.\n \nFor **Configuration presets**, choose custom configuration, now you have a load balancer for your environment with the capacity of at least one instance and at most four.\nI'd recommend at least 2 instances, to change that, on the **Capacity** card hit **Modify**. \n\n@@@ div { .centered-img }\n\n@@@\n\nChange the **Instances** to min 2, max 4 then hit **Save**. For the **Scaling triggers**, I'd keep the default values, but know that you can edit the capacity config any time you want, it only costs a redeploy, which will be done automatically by the way.\n \nInstances size is by default t2.micro, which is a bit small for running Otoroshi, I'd recommend a t2.medium. \nOn the **Instances** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFor **Instance type** choose t2.medium, then hit **Save**, no need to change the volume size, unless you have a lot of http call faults, which means a lot more logs, in that case the default volume size may not be enough.\n\nThe default environment created for Otoroshi, for instance Otoroshi-env, is a web server environment which fits in your case, but the thing is that on AWS Elastic Beanstalk by default a web server environment for a docker-based application, runs behind an Nginx proxy.\nWe have to remove that proxy. So on the **Software** card hit **Modify**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nFor **Proxy server** choose None then hit **Save**.\n\nAlso note that you can set Envs for Otoroshi in same page (see image below). \n\n@@@ div { .centered-img }\n\n@@@ \n\nTo finalise the creation process, hit **Create app** on the bottom right.\n\nThe Otoroshi app is now created, and it's running which is cool, but we still don't have neither a **datastore** nor **https**.\n \n## Create an Otoroshi datastore on AWS ElastiCache\n\nBy default Otoroshi uses non persistent memory to store it's data, Otoroshi supports many kinds of datastores. In this section we will be covering Redis datastore. \n\nBefore starting, using a datastore hosted by AWS is not at all mandatory, feel free to use your own if you like, but if you want to learn more about ElastiCache, this section may interest you, otherwise you can skip it.\n\nGo to [AWS ElastiCache](https://eu-west-3.console.aws.amazon.com/elasticache/home?region=eu-west-3#) and hit **Get Started Now**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nFor **Cluster engine** keep Redis.\n\nChoose a **Name** for your datastore, for instance otoroshi-datastore.\n\nYou can keep all the other default values and hit **Create** on the bottom right of the page.\n\nOnce your Redis Cluster is created, it would look like the image below.\n\n@@@ div { .centered-img }\n\n@@@ \n\n\nFor applications in the same security group as your cluster, redis cluster is accessible via the **Primary Endpoint**. Don't worry the default security group is fine, you don't need any configuration to access the cluster from Otoroshi.\n\nTo make Otoroshi use the created cluster, you can either use Envs `APP_STORAGE=redis`, `REDIS_HOST` and `REDIS_PORT`, or set `otoroshi.storage=redis`, `otoroshi.redis.host` and `otoroshi.redis.port` in your otoroshi.conf.\n\n## Create SSL certificate and configure your domain\n\nOtoroshi has now a datastore, but not yet ready for use. \n\nIn order to get it ready you need to :\n\n* Configure Otoroshi with your domain \n* Create a wildcard SSL certificate for your domain\n* Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n* Configure your DNS to redirect all traffic on your domain to Otoroshi \n \n### Configure Otoroshi with your domain\n\nYou can use ENVs or you can use a custom otoroshi.conf in your Docker container.\n\nFor the second option your otoroshi.conf would look like this :\n\n``` \n include \"application.conf\"\n http.port = 8080\n app {\n env = \"prod\"\n domain = \"mysubdomain.oto.tools\"\n rootScheme = \"https\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n \n storage = \"redis\"\n redis {\n host=\"myredishost\"\n port=myredisport\n }\n \n privateapps {\n subdomain = \"privateapps\"\n }\n \n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-client-id\"\n backOfficeApiKeyClientSecret = \"admin-client-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n proxy {\n https = true\n local = false\n }\n }\n claim {\n sharedKey = \"myclaimsharedkey\"\n }\n }\n \n play.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2147483646\n domain = \".mysubdomain.oto.tools\"\n cookieName = \"oto-sess\"\n }\n }\n``` \n\n### Create a wildcard SSL certificate for your domain\n\nGo to [AWS Certificate Manager](https://eu-west-3.console.aws.amazon.com/acm/home?region=eu-west-3#/firstrun).\n\nBelow **Provision certificates** hit **Get started**.\n\n@@@ div { .centered-img }\n\n@@@ \n \nKeep the default selected value **Request a public certificate** and hit **Request a certificate**.\n \n@@@ div { .centered-img }\n\n@@@ \n\nPut your **Domain name**, use *. for wildcard, for instance *\\*.mysubdomain.oto.tools*, then hit **Next**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nYou can choose between **Email validation** and **DNS validation**, I'd recommend **DNS validation**, then hit **Review**. \n \n@@@ div { .centered-img }\n\n@@@ \n \nVerify that you did put the right **Domain name** then hit **Confirm and request**. \n\n@@@ div { .centered-img }\n\n@@@\n \nAs you see in the image above, to let Amazon do the validation you have to add the `CNAME` record to your DNS configuration. Normally this operation takes around one day.\n \n### Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n\nOnce the certificate is validated, you need to modify the configuration of Otoroshi-env to add the SSL certificate for HTTPS. \nFor that you need to go to [AWS Elastic Beanstalk applications](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/applications),\nhit **Otoroshi-env**, then on the left side hit **Configuration**, then on the **Load balancer** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nIn the **Application Load Balancer** section hit **Add listener**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFill the popup as the image above, then hit **Add**. \n\nYou should now be seeing something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n \n \nMake sure that your listener is enabled, and on the bottom right of the page hit **Apply**.\n\nNow you have **https**, so let's use Otoroshi.\n\n### Configure your DNS to redirect all traffic on your domain to Otoroshi\n \nIt's actually pretty simple, you just need to add a `CNAME` record to your DNS configuration, that redirects *\\*.mysubdomain.oto.tools* to the DNS name of Otoroshi's load balancer.\n\nTo find the DNS name of Otoroshi's load balancer go to [AWS Ec2](https://eu-west-3.console.aws.amazon.com/ec2/v2/home?region=eu-west-3#LoadBalancers:tag:elasticbeanstalk:environment-name=Otoroshi-env;sort=loadBalancerName)\n\nYou would find something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n\nThere is your DNS name, so add your `CNAME` record. \n \nOnce all these steps are done, the AWS Elastic Beanstalk Otoroshi instance, would now be handling all the requests on your domain. ;) \n"},{"name":"clever-cloud.md","id":"/deploy/clever-cloud.md","url":"/deploy/clever-cloud.html","title":"Clever-Cloud","content":"# Clever-Cloud\n\nNow you want to use Otoroshi on Clever Cloud. Otoroshi has been designed and created to run on Clever Cloud and a lot of choices were made because of how Clever Cloud works.\n\n## Create an Otoroshi instance on CleverCloud\n\nIf you want to customize the configuration @ref:[use env. variables](../install/setup-otoroshi.md#configuration-with-env-variables), you can use [the example provided below](#example-of-clevercloud-env-variables)\n\nCreate a new CleverCloud app based on a clevercloud git repo (not empty) or a github project of your own (not empty).\n\n@@@ div { .centered-img }\n\n@@@\n\nThen choose what kind of app your want to create, for Otoroshi, choose `Java + Jar`\n\n@@@ div { .centered-img }\n\n@@@\n\nNext, set up choose instance size and auto-scalling. Otoroshi can run on small instances, especially if you just want to test it.\n\n@@@ div { .centered-img }\n\n@@@\n\nFinally, choose a name for your app\n\n@@@ div { .centered-img }\n\n@@@\n\nNow you just need to customize environnment variables\n\nat this point, you can also add other env. variables to configure Otoroshi like in [the example provided below](#example-of-clevercloud-env-variables)\n\n@@@ div { .centered-img }\n\n@@@\n\nYou can also use expert mode :\n\n@@@ div { .centered-img }\n\n@@@\n\nNow, your app is ready, don't forget to add a custom domains name on the CleverCloud app matching the Otoroshi app domain. \n\n## Example of CleverCloud env. variables\n\nYou can add more env variables to customize your Otoroshi instance like the following. Use the expert mode to copy/paste all the values in one shot. If you want an real datastore, create a redis addon on clevercloud, link it to your otoroshi app and change the `APP_STORAGE` variable to `redis`\n\n
      \n\n
      \n```\nADMIN_API_CLIENT_ID=xxxx\nADMIN_API_CLIENT_SECRET=xxxxx\nADMIN_API_GROUP=xxxxxx\nADMIN_API_SERVICE_ID=xxxxxxx\nCLAIM_SHAREDKEY=xxxxxxx\nOTOROSHI_INITIAL_ADMIN_LOGIN=youremailaddress\nOTOROSHI_INITIAL_ADMIN_PASSWORD=yourpassword\nPLAY_CRYPTO_SECRET=xxxxxx\nSESSION_NAME=oto-session\nAPP_DOMAIN=yourdomain.tech\nAPP_ENV=prod\nAPP_STORAGE=inmemory\nAPP_ROOT_SCHEME=https\nCC_PRE_BUILD_HOOK=curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/${latest_otoroshi_version}/otoroshi.jar'\nCC_JAR_PATH=./otoroshi.jar\nCC_JAVA_VERSION=11\nPORT=8080\nSESSION_DOMAIN=.yourdomain.tech\nSESSION_MAX_AGE=604800000\nSESSION_SECURE_ONLY=true\nUSER_AGENT=otoroshi\nMAX_EVENTS_SIZE=1\nWEBHOOK_SIZE=100\nAPP_BACKOFFICE_SESSION_EXP=86400000\nAPP_PRIVATEAPPS_SESSION_EXP=86400000\nENABLE_METRICS=true\nOTOROSHI_ANALYTICS_PRESSURE_ENABLED=true\nUSE_CACHE=true\n```\n
      "},{"name":"clustering.md","id":"/deploy/clustering.md","url":"/deploy/clustering.html","title":"Otoroshi clustering","content":"# Otoroshi clustering\n\nOtoroshi can work as a cluster by default as you can spin many Otoroshi servers using the same datastore or datastore cluster. In that case any instance is capable of serving services, Otoroshi admin UI, Otoroshi admin API, etc.\n\nBut sometimes, this is not enough. So Otoroshi provides an additional clustering model named `Leader / Workers` where there is a leader cluster ([control plane](https://en.wikipedia.org/wiki/Control_plane)), composed of Otoroshi instances backed by a datastore like Redis, PostgreSQL or Cassandra, that is in charge of all `writes` to the datastore through Otoroshi admin UI and API, and a worker cluster ([data plane](https://en.wikipedia.org/wiki/Forwarding_plane)) composed of horizontally scalable Otoroshi instances, backed by a super fast in memory datastore, with the sole purpose of routing traffic to your services based on data synced from the leader cluster. With this distributed Otoroshi version, you can reach your goals of high availability, scalability and security.\n\nOtoroshi clustering only uses http internally (right now) to make communications between leaders and workers instances so it is fully compatible with PaaS providers like [Clever-Cloud](https://www.clever-cloud.com/en/) that only provide one external port for http traffic.\n\n@@@ div { .centered-img }\n\n\n*Fig. 1: Simplified view*\n@@@\n\n@@@ div { .centered-img }\n\n\n*Fig. 2: Deployment view*\n@@@\n\n## Cluster configuration\n\n```hocon\notoroshi {\n cluster {\n mode = \"leader\" # can be \"off\", \"leader\", \"worker\"\n compression = 4 # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9\n leader {\n name = ${?CLUSTER_LEADER_NAME} # name of the instance, if none, it will be generated\n urls = [\"http://127.0.0.1:8080\"] # urls to contact the leader cluster\n host = \"otoroshi-api.oto.tools\" # host of the otoroshi api in the leader cluster\n clientId = \"apikey-id\" # otoroshi api client id\n clientSecret = \"secret\" # otoroshi api client secret\n cacheStateFor = 4000 # state is cached during (ms)\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME} # name of the instance, if none, it will be generated\n retries = 3 # number of retries when calling leader cluster\n timeout = 2000 # timeout when calling leader cluster\n state {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on state sync\n pollEvery = 10000 # interval of time (ms) between 2 state sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on state sync\n }\n quotas {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on quotas sync\n pushEvery = 2000 # interval of time (ms) between 2 quotas sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on quotas sync\n }\n }\n }\n}\n```\n\nyou can also use many env. variables to configure Otoroshi cluster\n\n```hocon\notoroshi {\n cluster {\n mode = ${?CLUSTER_MODE}\n compression = ${?CLUSTER_COMPRESSION}\n leader {\n name = ${?CLUSTER_LEADER_NAME}\n host = ${?CLUSTER_LEADER_HOST}\n url = ${?CLUSTER_LEADER_URL}\n clientId = ${?CLUSTER_LEADER_CLIENT_ID}\n clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET}\n groupingBy = ${?CLUSTER_LEADER_GROUP_BY}\n cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR}\n stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH}\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME}\n retries = ${?CLUSTER_WORKER_RETRIES}\n timeout = ${?CLUSTER_WORKER_TIMEOUT}\n state {\n retries = ${?CLUSTER_WORKER_STATE_RETRIES}\n pollEvery = ${?CLUSTER_WORKER_POLL_EVERY}\n timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT}\n }\n quotas {\n retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES}\n pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY}\n timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT}\n }\n }\n }\n}\n```\n\n@@@ warning\nYou **should** use HTTPS exposition for the Otoroshi API that will be used for data sync as sensitive informations are exchanged between control plane and data plane.\n@@@\n\n@@@ warning\nYou **must** have the same cluster configuration on every Otoroshi instance (worker/leader) with only names and mode changed for each instance. Some things in leader/worker are computed using configuration of their counterpart worker/leader.\n@@@\n\n## Cluster UI\n\nOnce an Otoroshi instance is launcher as cluster Leader, a new row of live metrics tile will be available on the home page of Otoroshi admin UI.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can also access a more detailed view of the cluster at `Settings (cog icon) / Cluster View`\n\n@@@ div { .centered-img }\n\n@@@\n\n## Run examples\n\nfor leader \n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nfor worker\n\n```sh\njava -Dhttp.port=8092 -Dhttps.port=9092 -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0=http://127.0.0.1:8091 -jar otoroshi.jar\n```\n\n## Setup a cluster by example\n\nif you want to see how to setup an otoroshi cluster, just check @ref:[the clustering tutorial](../how-to-s/setup-otoroshi-cluster.md)"},{"name":"index.md","id":"/deploy/index.md","url":"/deploy/index.html","title":"Deploy to production","content":"# Deploy to production\n\nNow it's time to deploy Otoroshi in production, in this chapter we will see what kind of things you can do.\n\nOtoroshi can run wherever you want, even on a raspberry pi (Cluster^^) ;)\n\n@@@div { .plugin .platform }\n\n## Cloud APIM\n\nCloud APIM provides Otoroshi instances as a service. You can easily create production ready Otoroshi clusters in just a few clics.\n\n\n[Documentation](https://www.cloud-apim.com/)\n@@@\n\n@@@div { .plugin .platform }\n\n## Clever Cloud\n\nOtoroshi provides an integration to create easily services based on application deployed on your Clever Cloud account.\n\n\n@ref:[Documentation](./clever-cloud.md)\n@@@\n\n@@@div { .plugin .platform } \n## Kubernetes\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support.\n\n\n\n@ref:[Documentation](./kubernetes.md)\n@@@\n\n@@@div { .plugin .platform } \n## AWS Elastic Beanstalk\n\nRun Otoroshi on AWS Elastic Beanstalk\n\n\n\n@ref:[Tutorial](./aws.md)\n@@@\n\n@@@div { .plugin .platform } \n## Amazon ECS\n\nDeploy the Otoroshi Docker image using Amazon Elastic Container Service\n\n\n\n@link:[Tutorial](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform }\n## GCE\n\nDeploy the Docker image using Google Compute Engine container integration\n\n\n\n@link:[Documentation](https://cloud.google.com/compute/docs/containers/deploying-containers)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform } \n## Azure\n\nDeploy the Docker image using Azure Container Service\n\n\n\n@link:[Documentation](https://azure.microsoft.com/en-us/services/container-service/)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker) \n@@@\n\n@@@div { .plugin .platform } \n## Heroku\n\nDeploy the Docker image using Docker integration\n\n\n\n@link:[Documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform } \n## CloudFoundry\n\nDeploy the Docker image using -Docker integration\n\n\n\n@link:[Documentation](https://docs.cloudfoundry.org/adminguide/docker.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform .platform-actions-column } \n## Your own infrastructure\n\nAs Otoroshi is a Play Framework application, you can read the doc about putting a `Play` app in production.\n\nDownload the latest Otoroshi distribution, unzip it, customize it and run it.\n\n@link:[Play Framework](https://www.playframework.com)\n@link:[Production Configuration](https://www.playframework.com/documentation/2.6.x/ProductionConfiguration)\n@ref:[Otoroshi distribution](../install/get-otoroshi.md#from-zip)\n@@@\n\n@@@div { .break }\n## Scaling and clustering in production\n@@@\n\n\n@@@div { .plugin .platform .dark-platform } \n## Clustering\n\nDeploy Otoroshi as a cluster of leaders and workers.\n\n\n@ref:[Documentation](./clustering.md)\n@@@\n\n@@@div { .plugin .platform .dark-platform } \n## Scaling Otoroshi\n\nOtoroshi is designed to be reasonably easy to scale and be highly available.\n\n\n@ref:[Documentation](./scaling.md) \n@@@\n\n@@@ index\n\n* [Clustering](./clustering.md)\n* [Kubernetes](./kubernetes.md)\n* [Clever Cloud](./clever-cloud.md)\n* [AWS - Elastic Beanstalk](./aws.md)\n* [Scaling](./scaling.md) \n\n@@@\n"},{"name":"kubernetes.md","id":"/deploy/kubernetes.md","url":"/deploy/kubernetes.html","title":"Kubernetes","content":"# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.14.0\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.14.0\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"},{"name":"scaling.md","id":"/deploy/scaling.md","url":"/deploy/scaling.html","title":"Scaling Otoroshi","content":"# Scaling Otoroshi\n\n## Using multiple instances with a front load balancer\n\nOtoroshi has been designed to work with multiple instances. If you already have an infrastructure using frontal load balancing, you just have to declare Otoroshi instances as the target of all domain names handled by Otoroshi\n\n## Using master / workers mode of Otoroshi\n\nYou can read everything about it in @ref:[the clustering section](../deploy/clustering.md) of the documentation.\n\n## Using IPVS\n\nYou can use [IPVS](https://en.wikipedia.org/wiki/IP_Virtual_Server) to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi. You can find example of configuration [here](http://www.linuxvirtualserver.org/VS-DRouting.html) \n\n## Using DNS Round Robin\n\nYou can use [DNS round robin technique](https://en.wikipedia.org/wiki/Round-robin_DNS) to declare multiple A records under the domain names handled by Otoroshi.\n\n## Using software L4/L7 load balancers\n\nYou can use software L4 load balancers like NGINX or HAProxy to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi.\n\nNGINX L7\n: @@snip [nginx-http.conf](../snippets/nginx-http.conf) \n\nNGINX L4\n: @@snip [nginx-tcp.conf](../snippets/nginx-tcp.conf) \n\nHA Proxy L7\n: @@snip [haproxy-http.conf](../snippets/haproxy-http.conf) \n\nHA Proxy L4\n: @@snip [haproxy-tcp.conf](../snippets/haproxy-tcp.conf) \n\n## Using a custom TCP load balancer\n\nYou can also use any other TCP load balancer, from a hardware box to a small js file like\n\ntcp-proxy.js\n: @@snip [tcp-proxy.js](../snippets/tcp-proxy.js) \n\ntcp-proxy.rs\n: @@snip [tcp-proxy.rs](../snippets/proxy.rs) \n\n"},{"name":"dev.md","id":"/dev.md","url":"/dev.html","title":"Developing Otoroshi","content":"# Developing Otoroshi\n\nIf you want to play with Otoroshis code, here are some tips\n\n## The tools\n\nYou will need\n\n* git\n* JDK >= 11\n* SBT >= 1.7+\n* Node 18+ & yarn 1.x\n\n## Clone the repository\n\n```sh\ngit clone https://github.com/MAIF/otoroshi.git\n```\n\nor fork otoroshi and clone your own repository.\n\n## Run otoroshi in dev mode\n\nto run otoroshi in dev mode, you'll need to run two separate process to serve the javascript UI and the server part.\n\n### Javascript side\n\njust go to `/otoroshi/javascript` and install the dependencies with\n\n```sh\nyarn install\n# or\nnpm install\n```\n\nthen run the dev server with\n\n```sh\nyarn start\n# or\nnpm run start\n```\n\n### Server side\n\nsetup SBT opts with\n\n```sh\nexport SBT_OPTS=\"-Xmx2G -Xss6M\"\n```\n\nthen just go to `/otoroshi` and run the sbt console with \n\n```sh\nsbt\n```\n\nthen in the sbt console run the following command\n\n```sh\n~reStart\n# to pass jvm args, you can use: ~reStart --- -Dotoroshi.storage=memory ...\n```\n\nyou can now access your otoroshi instance at `http://otoroshi.oto.tools:9999`\n\n## Test otoroshi\n\nto run otoroshi test just go to `/otoroshi` and run the main test suite with\n\n```sh\nsbt 'testOnly OtoroshiTests'\n```\n\n## Create a release\n\njust go to `/otoroshi/javascript` and then build the UI\n\n```sh\nyarn install\nyarn build\n```\n\nthen go to `/otoroshi` and build the otoroshi distribution\n\n```sh\nsbt ';clean;compile;dist;assembly'\n```\n\nthe otoroshi build is waiting for you in `/otoroshi/target/scala-2.12/otoroshi.jar` or `/otoroshi/target/universal/otoroshi-1.x.x.zip`\n\n## Build the documentation\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/doc.sh all\n```\n\nThe documentation is located at `manual/target/paradox/site/main/`\n\n## Format the sources\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/fmt.sh\n```\n"},{"name":"apikeys.md","id":"/entities/apikeys.md","url":"/entities/apikeys.html","title":"Apikeys","content":"# Apikeys\n\nAn API key is a unique identifier used to connect to, or perform, an route call. \n\n@@@ div { .centered-img }\n\n@@@\n\nYou can found a concrete example @ref:[here](../how-to-s/secure-with-apikey.md)\n\n* `ApiKey Id`: the id is a unique random key that will represent this API key\n* `ApiKey Secret`: the secret is a random key used to validate the API key\n* `ApiKey Name`: a name for the API key, used for debug purposes\n* `ApiKey description`: a useful description for this apikey\n* `Valid until`: auto disable apikey after this date\n* `Enabled`: if the API key is disabled, then any call using this API key will fail\n* `Read only`: if the API key is in read only mode, every request done with this api key will only work for GET, HEAD, OPTIONS verbs\n* `Allow pass by clientid only`: here you allow client to only pass client id in a specific header in order to grant access to the underlying api\n* `Constrained services only`: this apikey can only be used on services using apikey routing constraints\n* `Authorized on`: the groups/services linked to this api key\n\n### Metadata and tags\n\n* `Tags`: tags attached to the api key\n* `Metadata`: metadata attached to the api key\n\n### Automatic secret rotation\n\nAPI can handle automatic secret rotation by themselves. When enabled, the rotation changes the secret every `Rotation every` duration. During the `Grace period` both secret will be usable.\n \n* `Enabled`: enabled automatic apikey secret rotation\n* `Rotation every`: rotate secrets every\n* `Grace period`: period when both secrets can be used\n* `Next client secret`: display the next generated client secret\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Call examples\n\n* `Curl Command`: simple request with the api key passed by header\n* `Basic Auth. Header`: authorization Header with the api key as base64 encoded format\n* `Curl Command with Basic Auth. Header`: simple request with api key passed in the Authorization header as base64 format\n\n### Quotas\n\n* `Throttling quota`: the authorized number of calls per second\n* `Daily quota`: the authorized number of calls per day\n* `Monthly quota`: the authorized number of calls per month\n\n@@@ warning\n\nDaily and monthly quotas are based on the following rules :\n\n* daily quota is computed between 00h00:00.000 and 23h59:59.999 of the current day\n* monthly qutoas is computed between the first day of the month at 00h00:00.000 and the last day of the month at 23h59:59.999\n@@@\n\n### Quotas consumption\n\n* `Consumed daily calls`: the number of calls consumed today\n* `Remaining daily calls`: the remaining number of calls for today\n* `Consumed monthly calls`: the number of calls consumed this month\n* `Remaining monthly calls`: the remaining number of calls for this month\n\n"},{"name":"auth-modules.md","id":"/entities/auth-modules.md","url":"/entities/auth-modules.html","title":"Authentication modules","content":"# Authentication modules\n\nThe authentication modules manage the access to Otoroshi UI and can protect a route.\n\nA `private Otoroshi app` is an Otoroshi route with the Authentication plugin enabled.\n\nThe list of supported authentication are :\n\n* `OAuth 2.0/2.1` : an authorization standard that allows a user to grant limited access to their resources on one site to another site, without having to expose their credentials\n* `OAuth 1.0a` : the original standard for access delegation\n* `In memory` : create users directly in Otoroshi with rights and metadata\n* `LDAP : Lightweight Directory Access Protocol` : connect users using a set of LDAP servers\n* `SAML V2 - Security Assertion Markup Language` : an open-standard, XML-based data format that allows businesses to communicate user authentication and authorization information to partner companies and enterprise applications their employees may use.\n\nAll authentication modules have a unique `id`, a `name` and a `description`.\n\nEach module has also the following fields : \n\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n* `HttpOnly`: if enabled, the cookie cannot be accessed through client side script, prevent cross-site scripting (XSS) by not revealing the cookie to a third party\n* `Secure`: if enabled, avoid to include cookie in an HTTP Request without secure channel, typically HTTPs.\n* `Session max. age`: duration until the session expired\n* `User validators`: a list of validator that will check if, a user that successfully logged in has the right to actually, pass otoroshi based on the content of it's profile. A validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value. The JSONPath will be applied on a document that will look like\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"randomId\": \"xxxxx\",\n \"name\": \"john.doe@otoroshi.io\",\n \"email\": \"john.doe@otoroshi.io\",\n \"authConfigId\": \"xxxxxxxx\",\n \"profile\": { // the profile shape depends heavily on the identity provider\n \"sub\": \"xxxxxx\",\n \"nickname\": \"john.doe\",\n \"name\": \"john.doe@otoroshi.io\",\n \"picture\": \"https://foo.bar/avatar.png\",\n \"updated_at\": \"2022-04-20T12:57:39.723Z\",\n \"email\": \"john.doe@otoroshi.io\",\n \"email_verified\": true,\n \"rights\": [\"one\", \"two\"]\n },\n \"token\": { // the token shape depends heavily on the identity provider\n \"access_token\": \"xxxxxx\",\n \"refresh_token\": \"yyyyyy\",\n \"id_token\": \"zzzzzz\",\n \"scope\": \"openid profile email address phone offline_access\",\n \"expires_in\": 86400,\n \"token_type\": \"Bearer\"\n },\n \"realm\": \"global-oauth-xxxxxxx\",\n \"otoroshiData\": {\n ...\n },\n \"createdAt\": 1650459462650,\n \"expiredAt\": 1650545862652,\n \"lastRefresh\": 1650459462650,\n \"metadata\": {},\n \"tags\": []\n}\n```\n\nthe expected value support some syntax tricks like \n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current user has the right `two`, you can write the following validator\n\n```js\n{\n \"path\": \"$.profile.rights\",\n \"value\": \"Contains(two)\"\n}\n```\n\n## OAuth 2.0 / OIDC provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check these tutorials : @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md) or @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n\n* `Use cookie`: If your OAuth2 provider does not support query param in redirect uri, you can use cookies instead\n* `Use json payloads`: the access token, sended to retrieve the user info, will be pass in body as JSON. If disabled, it will sended as Map.\n* `Enabled PKCE flow`: This way, a malicious attacker can only intercept the Authorization Code, and they cannot exchange it for a token without the Code Verifier.\n* `Disable wildcard on redirect URIs`: As of OAuth 2.1, query parameters on redirect URIs are no longer allowed\n* `Refresh tokens`: Automatically refresh access token using the refresh token if available\n* `Read profile from token`: if enabled, the user profile will be read from the access token, otherwise the user profile will be retrieved from the user information url\n* `Super admins only`: All logged in users will have super admins rights\n* `Client ID`: a public identifier of your app\n* `Client Secret`: a secret known only to the application and the authorization server\n* `Authorize URL`: used to interact with the resource owner and get the authorization to access the protected resource\n* `Token URL`: used by the application in order to get an access token or a refresh token\n* `Introspection URL`: used to validate access tokens\n* `Userinfo URL`: used to retrieve the profile of the user\n* `Login URL`: used to redirect user to the login provider page\n* `Logout URL`: redirect uri used by the identity provider to redirect user after logging out\n* `Callback URL`: redirect uri sended to the identity provider to redirect user after successfully connecting\n* `Access token field name`: field used to search access token in the response body of the token URL call\n* `Scope`: presented scopes to the user in the consent screen. Scopes are space-separated lists of identifiers used to specify what access privileges are being requested\n* `Claims`: asked name/values pairs that contains information about a user.\n* `Name field name`: Retrieve name from token field\n* `Email field name`: Retrieve email from token field\n* `Otoroshi metadata field name`: Retrieve metadata from token field\n* `Otoroshi rights field name`: Retrieve user rights from user profile\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n* `Api key metadata field name`: used to extract api key metadata from the OIDC access token \n* `Api key tags field name`: used to extract api key tags from the OIDC access token \n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n* `OIDC config url`: URI of the openid-configuration used to discovery documents. By convention, this URI ends with `.well-known/openid-configuration`\n* `Token verification`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: The Hmac secret\n* `Base64 encoded secret`: Is the secret encoded with base64\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n## OAuth 1.0a provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)\n\n* `Http Method`: method used to get request token and the access token \n* `Consumer key`: the identifier portion of the client credentials (equivalent to a username)\n* `Consumer secret`: the identifier portion of the client credentials (equivalent to a password)\n* `Request Token URL`: url to retrieve the request token\n* `Authorize URL`: used to redirect user to the login page\n* `Access token URL`: used to retrieve the access token from the server\n* `Profile URL`: used to get the user profile\n* `Callback URL`: used to redirect user when successfully connecting\n* `Rights override`: override the rights of the connected user. With JSON format, each authenticated user, using email, can be associated to a list of rights on tenants and Otoroshi teams.\n\n## LDAP Authentication provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Allow empty password`: LDAP servers configured by default with the possibility to connect without password can be secured by this module to ensure that user provides a password\n* `Super admins only`: All logged in users will have super admins rights\n* `Extract profile`: extract LDAP profile in the Otoroshi user\n* `LDAP Server URL`: list of LDAP servers to join. Otoroshi use this list in sequence and swap to the next server, each time a server breaks in timeout\n* `Search Base`: used to global filter\n* `Users search base`: concat with search base to search users in LDAP\n* `Mapping group filter`: map LDAP groups with Otoroshi rights\n* `Search Filter`: used to filter users. *\\${username}* is replace by the email of the user and compare to the given field\n* `Admin username (bind DN)`: holds the name of the environment property for specifying the identity of the principal for authenticating the caller to the service\n* `Admin password`: holds the name of the environment property for specifying the credentials of the principal for authenticating the caller to the service\n* `Extract profile filters attributes in`: keep only attributes which are matching the regex\n* `Extract profile filters attributes not in`: keep only attributes which are not matching the regex\n* `Name field name`: Retrieve name from LDAP field\n* `Email field name`: Retrieve email from LDAP field\n* `Otoroshi metadata field name`: Retrieve metadata from LDAP field\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Additional rights group`: list of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n\n## In memory provider\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Login with WebAuthn` : enabled logging by WebAuthn\n* `Users`: list of users with *name*, *email* and *metadata*. The default password is *password*. The edit button is useful when you want to change the password of the user. The reset button reinitialize the password. \n* `Users raw`: show the registered users with their profile and their rights. You can edit directly each field, especially the rights of the user.\n\n## SAML v2 provider\n\n* `Single sign on URL`: the Identity Provider Single Sign-On URL\n* `The protocol binding for the login request`: the protocol binding for the login request\n* `Single Logout URL`: a SAML flow that allows the end-user to logout from a single session and be automatically logged out of all related sessions that were established during SSO\n* `The protocol binding for the logout request`: the protocol binding for the logout request\n* `Sign documents`: Should SAML Request be signed by Otoroshi ?\n* `Validate Assertions Signature`: Enable/disable signature validation of SAML assertions\n* `Validate assertions with Otoroshi certificate`: validate assertions with Otoroshi certificate. If disabled, the `Encryption Certificate` and `Encryption Private Key` fields can be used to pass a certificate and a private key to validate assertions.\n* `Encryption Certificate`: certificate used to verify assertions\n* `Encryption Private Key`: privaye key used to verify assertions\n* `Signing Certificate`: certicate used to sign documents\n* `Signing Private Key`: private key to sign documents\n* `Signature al`: the signature algorithm to use to sign documents\n* `Canonicalization Method`: canonicalization method for XML signatures \n* `Encryption KeyPair`: the keypair used to sign/verify assertions\n* `Name ID Format`: SP and IdP usually communicate each other about a subject. That subject should be identified through a NAME-IDentifier, which should be in some format so that It is easy for the other party to identify it based on the Format\n* `Use NameID format as email`: use NameID format as email. If disabled, the email will be search from the attributes\n* `URL issuer`: provide the URL to the IdP's who will issue the security token\n* `Validate Signature`: enable/disable signature validation of SAML responses\n* `Validate Assertions Signature`: should SAML Assertions to be decrypted ?\n* `Validating Certificates`: the certificate in PEM format that must be used to check for signatures.\n\n## Special routes\n\nwhen using private apps with auth. modules, you can access special routes that can help you \n\n```sh \nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/logout' # trigger logout for the current auth. module\nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/me' # get the current logged user profile (do not forget to pass cookies)\n```\n\n## Related pages\n* @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n* @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md)\n* @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n* @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)"},{"name":"backends.md","id":"/entities/backends.md","url":"/entities/backends.html","title":"Backends","content":"# Backends\n\nA backend represent a list of server to target in a route and its client settings, load balancing, etc.\n\nThe backends can be define directly on the route designer or on their dedicated page in order to be reusable.\n\n## UI page\n\nYou can find all backends [here](http://otoroshi.oto.tools:8080/bo/dashboard/backends)\n\n## Global Properties\n\n* `Targets root path`: the path to add to each request sent to the downstream service \n* `Full path rewrite`: When enabled, the path of the uri will be totally stripped and replaced by the value of `Targets root path`. If this value contains expression language expressions, they will be interpolated before forwading the request to the backend. When combined with things like named path parameters, it is possible to perform a ful url rewrite on the target path like\n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Targets\n\nThe list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures.\n\n* `id`: unique id of the target\n* `Hostname`: the hostname of the target without scheme\n* `Port`: the port of the target\n* `TLS`: call the target via https\n* `Weight`: the weight of the target. This valus is used by the load balancing strategy to dispatch the traffic between all targets\n* `Predicate`: a function to filter targets from the target list based on a predefined predicate\n* `Protocol`: protocol used to call the target, can be only equals to `HTTP/1.0`, `HTTP/1.1`, `HTTP/2.0` or `HTTP/3.0`\n* `IP address`: the ip address of the target\n* `TLS Settings`:\n * `Enabled`: enable this section\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with the downstream service\n * `Trusted certificates`: list of trusted certificates received from the downstream service\n\n\n## Heatlh check\n\n* `Enabled`: if enabled, the health check URL will be called at regular intervals\n* `URL`: the URL to call to run the health check\n\n## Load balancing\n\n* `Type`: the load balancing algorithm used\n\n## Client settings\n\n* `backoff factor`: specify the factor to multiply the delay for each retry (default value 2)\n* `retries`: specify how many times the client will retry to fetch the result of the request after an error before giving up. (default value 1)\n* `max errors`: specify how many errors can pass before opening the circuit breaker (default value 20)\n* `global timeout`: specify how long the global call (with retries) should last at most in milliseconds. (default value 30000)\n* `connection timeout`: specify how long each connection should last at most in milliseconds. (default value 10000)\n* `idle timeout`: specify how long each connection can stay in idle state at most in milliseconds (default value 60000)\n* `call timeout`: Specify how long each call should last at most in milliseconds. (default value 30000)\n* `call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response. (default value 120000)\n* `initial delay`: delay after which first retry will happens if needed (default value 50)\n* `sample interval`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor (default value 2000)\n* `cache connection`: try to keep tcp connection alive between requests (default value false)\n* `cache connection queue size`: queue size for an open tcp connection (default value 2048)\n* `custom timeouts` (list): \n * `Path`: the path on which the timeout will be active\n * `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n * `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n * `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n * `Call timeout`: Specify how long each call should last at most in milliseconds.\n * `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n## Proxy\n\n* `host`: host of proxy behind the identify provider\n* `port`: port of proxy behind the identify provider\n* `protocol`: protocol of proxy behind the identify provider\n* `principal`: user of proxy \n* `password`: password of proxy\n"},{"name":"certificates.md","id":"/entities/certificates.md","url":"/entities/certificates.html","title":"Certificates","content":"# Certificates\n\nAll generated and imported certificates are listed in the `https://otoroshi.xxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe list of available actions are:\n\n* `Add item`: redirects the user on the certificate creation page. It's useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* `Let's Encrypt certificate`: asks a certificate matching a given host to Let's encrypt \n* `Create certificate`: issues a certificate with an existing Otoroshi certificate as CA.\n* `Import .p12 file`: loads a p12 file as certificate\n\n## Add item\n\n* `Id`: the generated unique id of the certificate\n* `Name`: the name of the certificate\n* `Description`: the description of the certificate\n* `Auto renew cert.`: certificate will be issued when it will be expired. Only works with a CA from Otoroshi and a known private key\n* `Client cert.`: the certificate generated will be used to identicate a client to a server\n* `Keypair`: the certificate entity will be a pair of public key and private key.\n* `Public key exposed`: if true, the public key will be exposed on `http://otoroshi-api.your-domain/.well-known/jwks.json`\n* `Certificate status`: the current status of the certificate. It can be valid if the certificate is not revoked and not expired, or equal to the reason of the revocation\n* `Certificate full chain`: list of certificates used to authenticate a client or a server\n* `Certificate private key`: the private key of the certificate or nothing if wanted. You can omit it if you want just add a certificte full chain to trust them.\n* `Private key password`: the password to protect the private key\n* `Certificate tags`: the tags attached to the certificate\n* `Certaificate metadata`: the metadata attached to the certificate\n\n## Let's Encrypt certificate\n\n* `Let's encrypt`: if enabled, the certificate will be generated by Let's Encrypt. If disabled, the user will be redirect to the `Create certificate` page\n* `Host`: the host send to Let's encrypt to issue the certificate\n\n## Create certificate view\n\n* `Issuer`: the CA used to sign your certificate\n* `CA certificate`: if enabled, the certificate will be used as an authority certificate. Once generated, it will be use as CA to sign the new certificates\n* `Let's Encrypt`: redirects to the Let's Encrypt page to request a certificate\n* `Client certificate`: the certificate generated will be used to identicate a client to a server\n* `Include A.I.A`: include authority information access urls in the certificate\n* `Key Type`: the type of the private key\n* `Key Size`: the size of the private key\n* `Signature Algorithm`: the signature algorithm used to sign the certificate\n* `Digest Algorithm`: the digest algorithm used\n* `Validity`: how much time your certificate will be valid\n* `Subject DN`: the subject DN of your certificate\n* `Hosts`: the hosts of your certificate\n\n"},{"name":"data-exporters.md","id":"/entities/data-exporters.md","url":"/entities/data-exporters.html","title":"Data exporters","content":"# Data exporters\n\nThe data exporters are the way to export alerts and events from Otoroshi to an external storage.\n\nTo try them, you can folllow @ref[this tutorial](../how-to-s/export-alerts-using-mailgun.md).\n\n## Common fields\n\n* `Type`: the type of event exporter\n* `Enabled`: enabled or not the exporter\n* `Name`: given name to the exporter\n* `Description`: the data exporter description\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nAll exporters are split in three parts. The first and second parts are common and the last are specific by exporter.\n\n* `Filtering and projection` : section to filter the list of sent events and alerts. The projection field allows you to export only certain event fields and reduce the size of exported data. It's composed of `Filtering` and `Projection` fields. To get a full usage of this elements, read @ref:[this section](#matching-and-projections)\n* `Queue details`: set of fields to adjust the workers of the exporter. \n * `Buffer size`: if elements are pushed onto the queue faster than the source is consumed the overflow will be handled with a strategy specified by the user. Keep in memory the number of events.\n * `JSON conversion workers`: number of workers used to transform events to JSON format in paralell\n * `Send workers`: number of workers used to send transformed events\n * `Group size`: chunk up this stream into groups of elements received within a time window (the time window is the next field)\n * `Group duration`: waiting time before sending the group of events. If the group size is reached before the group duration, the events will be instantly sent\n \nFor the last part, the `Exporter configuration` will be detail individually.\n\n## Matching and projections\n\n**Filtering** is used to **include** or **exclude** some kind of events and alerts. For each include and exclude field, you can add a list of key-value. \n\nLet's say we only want to keep Otoroshi alerts\n```json\n{ \"include\": [{ \"@type\": \"AlertEvent\" }] }\n```\n\nOtoroshi provides a list of rules to keep only events with specific values. We will use the following event to illustrate.\n\n```json\n{\n \"foo\": \"bar\",\n \"type\": \"AlertEvent\",\n \"alert\": \"big-alert\",\n \"status\": 200,\n \"codes\": [\"a\", \"b\"],\n \"inner\": {\n \"foo\": \"bar\",\n \"bar\": \"foo\"\n }\n}\n```\n\nThe rules apply with the previous example as event.\n\n@@@div { #filtering }\n \n@@@\n\n\n\n**Projection** is a list of fields to export. In the case of an empty list, all the fields of an event will be exported. In other case, **only** the listed fields will be exported.\n\nLet's say we only want to keep Otoroshi alerts and only type, timestamp and id of each exported events\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nAn other possibility is to **rename** the exported field. This value will be the same but the exported field will have a different name.\n\nLet's say we want to rename all `@id` field with `unique-id` as key\n\n```json\n{ \"@id\": \"unique-id\" }\n```\n\nThe last possiblity is to retrieve a sub-object of an event. Let's say we want to get the name of each exported user of events.\n\n```json\n{ \"user\": { \"name\": true } }\n```\n\nYou can also expand the entire source object with \n\n```json\n{\n \"$spread\": true\n}\n```\n\nand the remove fields you don't want with \n\n```json\n{\n \"fieldthatidontwant\": false\n}\n```\n\nProjections allows object modification using jspath, for instance, this example will create a new `otoroshiHeaderKeys` field to exported events. This field will contains a string array containing every request header name.\n\n```json\n{\n \"otoroshiHeaderKeys\": {\n \"$path\": \"$.otoroshiHeadersIn.*.key\"\n }\n}\n```\n\nAlternativerly, projections also allow to use JQ to transform exported events\n\n```json\n{\n \"headerKeys\": {\n \"$jq\": \"[.headers[].key]\"\n }\n}\n```\n\nJQ filter also allows conditionnal filtering : transformation is applied only if given predicate is match. In the following example, `headerKeys` field will be valued only if `target.scheme` is `https`.\n\n```json\n{\n \"headerKeys\": {\n \"$jqIf\": {\n \"filter\": \"[.headers[].key]\",\n \"predicate\": {\n \"path\": \"target.scheme\",\n \"value\": \"https\"\n }\n }\n }\n}\n```\n\nSee [JQ manual](https://jqlang.github.io/jq/manual/) for complete syntax reference.\n\n## Elastic\n\nWith this kind of exporter, every matching event will be sent to an elastic cluster (in batch). It is quite useful and can be used in combination with [elastic read in global config](./global-config.html#analytics-elastic-dashboard-datasource-read-)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with checked point, and if the case of it's successfull, it will displayed the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template presents in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n## Webhook \n\nWith this kind of exporter, every matching event will be sent to a URL (in batch) using a POST method and an JSON array body.\n\n* `Alerts hook URL`: url used to post events\n* `Hook Headers`: headers add to the post request\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n## Pulsar \n\nWith this kind of exporter, every matching event will be sent to an [Apache Pulsar topic](https://pulsar.apache.org/)\n\n\n* `Pulsar URI`: URI of the pulsar server\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n* `Pulsar tenant`: tenant on the pulsar server\n* `Pulsar namespace`: namespace on the pulsar server\n* `Pulsar topic`: topic on the pulsar server\n\n## Kafka \n\nWith this kind of exporter, every matching event will be sent to an [Apache Kafka topic](https://kafka.apache.org/). You can find few @ref[tutorials](../how-to-s/communicate-with-kafka.md) about the connection between Otoroshi and Kafka based on docker images.\n\n* `Kafka Servers`: the list of servers to contact to connect the Kafka client with the Kafka cluster\n* `Kafka topic`: the topic on which Otoroshi alerts will be sent\n\nBy default, Kafka is installed with no authentication. Otoroshi supports the following authentication mechanisms and protocols for Kafka brokers.\n\n### SASL\n\nThe Simple Authentication and Security Layer (SASL) [RFC4422] is a\nmethod for adding authentication support to connection-based\nprotocols.\n\n* `SASL username`: the client username \n* `SASL password`: the client username \n* `SASL Mechanism`: \n * `PLAIN`: SASL/PLAIN uses a simple username and password for authentication.\n * `SCRAM-SHA-256` and `SCRAM-SHA-512`: SASL/SCRAM uses usernames and passwords stored in ZooKeeper. Credentials are created during installation.\n\n### SSL \n\n* `Kafka keypass`: the keystore password if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka keystore path`: the keystore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka truststore path`: the truststore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Custom TLS Settings`: enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n### SASL + SSL\n\nThis mechanism uses the SSL configuration and the SASL configuration.\n\n## Mailer \n\nWith this kind of exporter, every matching event will be sent in batch as an email (using one of the following email provider)\n\nOtoroshi supports 5 exporters of email type.\n\n### Console\n\nNothing to add. The events will be write on the standard output.\n\n### Generic\n\n* `Mailer url`: URL used to push events\n* `Headers`: headers add to the push requests\n* `Email addresses`: recipients of the emails\n\n### Mailgun\n\n* `EU`: is EU server ? if enabled, *https://api.eu.mailgun.net/* will be used, otherwise, the US URL will be used : *https://api.mailgun.net/*\n* `Mailgun api key`: API key of the mailgun account\n* `Mailgun domain`: domain name of the mailgun account\n* `Email addresses`: recipients of the emails\n\n### Mailjet\n\n* `Public api key`: public key of the mailjet account\n* `Private api key`: private key of the mailjet account\n* `Email addresses`: recipients of the emails\n\n### Sendgrid\n\n* `Sendgrid api key`: api key of the sendgrid account\n* `Email addresses`: recipients of the emails\n\n## File \n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n\n## GoReplay file\n\nWith this kind of exporter, every matching event will be sent to a `.gor` file compatible with [GoReplay](https://goreplay.org/). \n\n@@@ warning\nthis exporter will only be able to catch `TrafficCaptureEvent`. Those events are created when a route (or the global config) of the @ref:[new proxy engine](../topics/engine.md) is setup to capture traffic using the `capture` flag.\n@@@\n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n* `Capture requests`: capture http requests in the `.gor` file\n* `Capture responses`: capture http responses in the `.gor` file\n\n## Console \n\nNothing to add. The events will be write on the standard output.\n\n## Custom \n\nThis type of exporter let you the possibility to write your own exporter with your own rules. To create an exporter, we need to navigate to the plugins page, and to create a new item of type exporter.\n\nWhen it's done, the exporter will be visible in this list.\n\n* `Exporter config.`: the configuration of the custom exporter.\n\n## Metrics \n\nThis plugin is useful to rewrite the metric labels exposed on the `/metrics` endpoint.\n\n* `Labels`: list of metric labels. Each pair contains an existing field name and the new name."},{"name":"global-config.md","id":"/entities/global-config.md","url":"/entities/global-config.html","title":"Global config","content":"# Global config\n\nThe global config, named `Danger zone` in Otoroshi, is the place to configure Otoroshi globally. \n\n> Warning: In this page, the configuration is really sensitive and affects the global behaviour of Otoroshi.\n\n\n### Misc. Settings\n\n\n* `Maintenance mode` : It passes every single service in maintenance mode. If a user calls a service, the maintenance page will be displayed\n* `No OAuth login for BackOffice` : Forces admins to login only with user/password or user/password/u2F device\n* `API Read Only`: Freeze Otoroshi datastore in read only mode. Only people with access to the actual underlying datastore will be able to disable this.\n* `Auto link default` : When no group is specified on a service, it will be assigned to default one\n* `Use circuit breakers` : Use circuit breaker on all services\n* `Use new http client as the default Http client` : All http calls will use the new http client by default\n* `Enable live metrics` : Enable live metrics in the Otoroshi cluster. Performs a lot of writes in the datastore\n* `Digitus medius` : Use middle finger emoji as a response character for endless HTTP responses (see [IP address filtering settings](#ip-address-filtering-settings)).\n* `Limit conc. req.` : Limit the number of concurrent request processed by Otoroshi to a certain amount. Highly recommended for resilience\n* `Use X-Forwarded-* headers for routing` : When evaluating routing of a request, X-Forwarded-* headers will be used if presents\n* `Max conc. req.` : Maximum number of concurrent requests processed by otoroshi.\n* `Max HTTP/1.0 resp. size` : Maximum size of an HTTP/1.0 response in bytes. After this limit, response will be cut and sent as is. The best value here should satisfy (maxConcurrentRequests * maxHttp10ResponseSize) < process.memory for worst case scenario.\n* `Max local events` : Maximum number of events stored.\n* `Lines` : *deprecated* \n\n### IP address filtering settings\n\n* `IP allowed list`: Only IP addresses that will be able to access Otoroshi exposed services\n* `IP blocklist`: IP addresses that will be refused to access Otoroshi exposed services\n* `Endless HTTP Responses`: IP addresses for which each request will return around 128 Gb of 0s\n\n\n### Quotas settings\n\n* `Global throttling`: The max. number of requests allowed per second globally on Otoroshi\n* `Throttling per IP`: The max. number of requests allowed per second per IP address globally on Otoroshi\n\n### Analytics: Elastic dashboard datasource (read)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with a connection checklist, if connection is successfull, it will display the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template present in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes over time. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `TrustAll`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with elasticsearch\n* `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n### Statsd settings\n\n* `Datadog agent`: The StatsD agent is a Datadog agent\n* `StatsD agent host`: The host on which StatsD agent is listening\n* `StatsD agent port`: The port on which StatsD agent is listening (default is 8125)\n\n\n### Backoffice auth. settings\n\n* `Backoffice auth. config`: the authentication module used in front of Otoroshi. It will be used to connect to Otoroshi on the login page\n\n### Let's encrypt settings\n\n* `Enabled`: when enabled, Otoroshi will have the possiblity to sign certificate from let's encrypt notably in the SSL/TSL Certificates page \n* `Server URL`: ACME endpoint of let's encrypt \n* `Email addresses`: (optional) list of addresses used to order the certificates \n* `Contact URLs`: (optional) list of addresses used to order the certificates \n* `Public Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n* `Private Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n\n\n### CleverCloud settings\n\nOnce configured, you can register one clever cloud app of your organization directly as an Otoroshi service.\n\n* `CleverCloud consumer key`: consumer key of your clever cloud OAuth 1.0 app\n* `CleverCloud consumer secret`: consumer secret of your clever cloud OAuth 1.0 app\n* `OAuth Token`: oauth token of your clever cloud OAuth 1.0 app\n* `OAuth Secret`: oauth token secret of your clever cloud OAuth 1.0 app \n* `CleverCloud orga. Id`: id of your clever cloud organization\n\n### Global scripts\n\nGlobal scripts will be deprecated soon, please use global plugins instead (see the next section)!\n\n### Global plugins\n\n* `Enabled`: enable the use of global plugins\n* `Plugins on new Otoroshi engine`: list of plugins used by the new Otoroshi engine\n* `Plugins on old Otoroshi engine`: list of plugins used by the old Otoroshi engine\n* `Plugin configuration`: the overloaded configuration of plugins\n\n### Proxies\n\nIn this section, you can add a list of proxies for :\n\n* Proxy for alert emails (mailgun)\n* Proxy for alert webhooks\n* Proxy for Clever-Cloud API access\n* Proxy for services access\n* Proxy for auth. access (OAuth, OIDC)\n* Proxy for client validators\n* Proxy for JWKS access\n* Proxy for elastic access\n\nEach proxy has the following fields \n\n* `Proxy host`: host of proxy\n* `Proxy port`: port of proxy\n* `Proxy principal`: user of proxy\n* `Proxy password`: password of proxy\n* `Non proxy host`: IP address that can access the service\n\n### Quotas alerting settings\n\n* `Enable quotas exceeding alerts`: When apikey quotas is almost exceeded, an alert will be sent \n* `Daily quotas threshold`: The percentage of daily calls before sending alerts\n* `Monthly quotas threshold`: The percentage of monthly calls before sending alerts\n\n### User-Agent extraction settings\n\n* `User-Agent extraction`: Allow user-agent details extraction. Can have impact on consumed memory. \n\n### Geolocation extraction settings\n\nExtract a geolocation for each call to Otoroshi.\n\n### Tls Settings\n\n* `Use random cert.`: Use the first available cert when none matches the current domain\n* `Default domain`: When the SNI domain cannot be found, this one will be used to find the matching certificate \n* `Trust JDK CAs (server)`: Trust JDK CAs. The CAs from the JDK CA bundle will be proposed in the certificate request when performing TLS handshake \n* `Trust JDK CAs (trust)`: Trust JDK CAs. The CAs from the JDK CA bundle will be used as trusted CAs when calling HTTPS resources \n* `Trusted CAs (server)`: Select the trusted CAs you want for TLS terminaison. Those CAs only will be proposed in the certificate request when performing TLS handshake \n\n\n### Auto Generate Certificates\n\n* `Enabled`: Generate certificates on the fly when they don't exist\n* `Reply Nicely`: When receiving request from a not allowed domain name, accept connection and display a nice error message \n* `CA`: certificate CA used to generate missing certificate\n* `Allowed domains`: Allowed domains\n* `Not allowed domains`: Not allowed domains\n \n\n### Global metadata\n\n* `Tags`: tags attached to the global config\n* `Metadata`: metadata attached to the global config\n\n### Actions at the bottom of the page\n\n* `Recover from a full export file`: Load global configuration from a previous export\n* `Full export`: Export with all created entities\n* `Full export (ndjson)`: Export your full state of database to ndjson format\n* `JSON`: Get the global config at JSON format \n* `YAML`: Get the global config at YAML format \n* `Enable Panic Mode`: Log out all users from UI and prevent any changes to the database by setting the admin Otoroshi api to read-only. The only way to exit of this mode is to disable this mode directly in the database. "},{"name":"index.md","id":"/entities/index.md","url":"/entities/index.html","title":"","content":"\n# Main entities\n\nIn this section, we will pass through all the main Otoroshi entities. Otoroshi entities are the main items stored in otoroshi datastore that will be used to configure routing, authentication, etc.\n\nAny entity has the following properties\n\n* **location** or **\\_loc**: the location of the entity (organization and team)\n* **id**: the id of the entity (except for apikeys)\n* **name**: the name of the entity\n* **description**: the description of the entity (optional)\n* **tags**: free tags that you can put on any entity to help you manage it, automate it, etc.\n* **metadata**: free key/value tuples that you can put on any entity to help you manage it, automate it, etc.\n\n@@@div { .entities }\n\n
      \nRoutes\nProxy your applications with routes\n
      \n@ref:[View](./routes.md)\n@@@\n\n@@@div { .entities }\n\n
      \nBackends\nReuse route targets\n
      \n@ref:[View](./backends.md)\n@@@\n\n@@@div { .entities }\n\n
      \nApikeys\nAdd security to your services using apikeys\n
      \n@ref:[View](./apikeys.md)\n@@@\n\n\n@@@div { .entities }\n\n
      \nOrganizations\nThis the most high level for grouping resources.\n
      \n@ref:[View](./organizations.md)\n@@@\n\n@@@div { .entities }\n\n
      \nTeams\nOrganize your resources by teams\n
      \n@ref:[View](./teams.md)\n@@@\n\n@@@div { .entities }\n\n
      \nService groups\nGroup your services\n
      \n@ref:[View](./service-groups.md)\n@@@\n\n@@@div { .entities }\n\n
      \nJWT verifiers\nVerify and forge token by services.\n
      \n@ref:[View](./jwt-verifiers.md)\n@@@\n\n@@@div { .entities }\n\n
      \nGlobal Config\nThe danger zone of Otoroshi\n
      \n@ref:[View](./global-config.md)\n@@@\n\n@@@div { .entities }\n\n
      \nTCP services\n\n
      \n@ref:[View](./tcp-services.md)\n@@@\n\n@@@div { .entities }\n\n
      \nAuth. modules\nSecure the Otoroshi UI and your web apps\n
      \n@ref:[View](./auth-modules.md)\n@@@\n\n@@@div { .entities }\n\n
      \nCertificates\nAdd secure communication between Otoroshi, clients and services\n
      \n@ref:[View](./certificates.md)\n@@@\n\n@@@div { .entities }\n\n
      \nData exporters\nExport alerts, events ands logs\n
      \n@ref:[View](./data-exporters.md)\n@@@\n\n@@@div { .entities }\n\n
      \nScripts\n\n
      \n@ref:[View](./scripts.md)\n@@@\n\n@@@div { .entities }\n\n
      \nService descriptors\nProxy your applications with service descriptors\n
      \n@ref:[View](./service-descriptors.md)\n@@@\n\n@@@ index\n\n* [Routes](./routes.md)\n* [Backends](./backends.md)\n* [Organizations](./organizations.md)\n* [Teams](./teams.md)\n* [Global Config](./global-config.md)\n* [Apikeys](./apikeys.md)\n* [Service groups](./service-groups.md)\n* [Auth. modules](./auth-modules.md)\n* [Certificates](./certificates.md)\n* [JWT verifiers](./jwt-verifiers.md)\n* [Data exporters](./data-exporters.md)\n* [Scripts](./scripts.md)\n* [TCP services](./tcp-services.md)\n* [Service descriptors](./service-descriptors.md)\n\n@@@\n"},{"name":"jwt-verifiers.md","id":"/entities/jwt-verifiers.md","url":"/entities/jwt-verifiers.html","title":"JWT verifiers","content":"# JWT verifiers\n\nSometimes, it can be pretty useful to verify Jwt tokens coming from other provider on some services. Otoroshi provides a tool to do that per service.\n\n* `Name`: name of the JWT verifier\n* `Description`: a simple description\n* `Strict`: if not strict, request without JWT token will be allowed to pass. This option is helpful when you want to force the presence of tokens in each request on a specific service \n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nEach JWT verifier is configurable in three steps : the `location` where find the token in incoming requests, the `validation` step to check the signature and the presence of claims in tokens, and the last step, named `Strategy`.\n\n## Token location\n\nAn incoming token can be found in three places.\n\n#### In query string\n\n* `Source`: JWT token location in query string\n* `Query param name`: the name of the query param where JWT is located\n\n#### In a header\n\n* `Source`: JWT token location in a header\n* `Header name`: the name of the header where JWT is located\n* `Remove value`: when the token is read, this value will be remove of header value (example: if the header value is *Bearer xxxx*, the *remove value* could be Bearer  don't forget the space at the end of the string)\n\n#### In a cookie\n\n* `Source`: JWT token location in a cookie\n* `Cookie name`: the name of the cookie where JWT is located\n\n## Token validation\n\nThis section is used to verify the extracted token from specified location.\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n\nAccording to the selected algorithm, the validation form will change.\n\n#### Hmac + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n#### RSASSA-PKCS1 + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the RSA public key\n* `Private key`: the RSA private key that can be empty if not used for JWT token signing\n\n#### ECDSA + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the ECDSA public key\n* `Private key`: the ECDSA private key that can be empty if not used for JWT token signing\n\n#### RSASSA-PKCS1 + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n \n#### ECDSA + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n\n#### Otoroshi KeyPair from token kid (only for verification)\n* `Use only exposed keypairs`: if enabled, Otoroshi will only use the key pairs that are exposed on the well-known. If disabled, it will search on any registered key pairs.\n\n#### JWK Set (only for verification)\n\n* `URL`: the JWK set URL where the public keys are exposed\n* `HTTP call timeout`: timeout for fetching the keyset\n* `TTL`: cache TTL for the keyset\n* `HTTP Headers`: the HTTP headers passed\n* `Key type`: type of the key searched in the jwks\n\n*TLS settings for JWKS fetching*\n\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n*Proxy*\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n## Strategy\n\nThe first step is to select the verifier strategy. Otoroshi supports 4 types of JWT verifiers:\n\n* `Default JWT token` will add a token if no present. \n* `Verify JWT token` will only verifiy token signing and fields values if provided. \n* `Verify and re-sign JWT token` will verify the token and will re-sign the JWT token with the provided algo. settings. \n* `Verify, re-sign and transform JWT token` will verify the token, re-sign and will be able to transform the token.\n\nAll verifiers has the following properties: \n\n* `Verify token fields`: when the JWT token is checked, each field specified here will be verified with the provided value\n* `Verify token array value`: when the JWT token is checked, each field specified here will be verified if the provided value is contained in the array\n\n\n#### Default JWT token\n\n* `Strict`: if token is already present, the call will fail\n* `Default value`: list of claims of the generated token. These fields support raw values or language expressions. See the documentation about @ref:[the expression language](../topics/expression-language.md)\n\n#### Verify JWT token\n\nNo specific values needed. This kind of verifier needs only the two fields `Verify token fields` and `Verify token array value`.\n\n#### Verify and re-sign JWT token\n\nWhen `Verify and re-sign JWT token` is chosen, the `Re-sign settings` appear. All fields of `Re-sign settings` are the same of the `Token validation` section. The only difference is that the values are used to sign the new token and not to validate the token.\n\n\n#### Verify, re-sign and transform JWT token\n\nWhen `Verify, re-sign and transform JWT token` is chosen, the `Re-sign settings` and `Transformation settings` appear.\n\nThe `Re-sign settings` are used to sign the new token and has the same fields than the `Token validation` section.\n\nFor the `Transformation settings` section, the fields are:\n\n* `Token location`: the location where to find/set the JWT token\n* `Header name`: the name of the header where JWT is located\n* `Prepend value`: remove a value inside the header value\n* `Rename token fields`: when the JWT token is transformed, it is possible to change a field name, just specify origin field name and target field name\n* `Set token fields`: when the JWT token is transformed, it is possible to add new field with static values, just specify field name and value\n* `Remove token fields`: when the JWT token is transformed, it is possible to remove fields"},{"name":"organizations.md","id":"/entities/organizations.md","url":"/entities/organizations.html","title":"Organizations","content":"# Organizations\n\nThe resources of Otoroshi are grouped by `Organization`. This the highest level for grouping resources.\n\nAn organization have a unique `id`, a `name` and a `description`. As all Otoroshi resources, an Organization have a list of tags and metadata associated.\n\nFor example, you can use the organizations as a mean of :\n\n* to seperate resources by services or entities in your enterprise\n* to split internal and external usage of the resources (it's useful when you have a list of services deployed in your company and another one deployed by your partners)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of organizations\n\nTo visualize and edit the list of organizations, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/organizations` route or click on the cog icon and select the organizations button.\n\nOnce on the page, you can create a new item, edit an existing organization or delete an existing one.\n\n> When an organization is deleted, the resources associated are not deleted. On the other hand, the organization and the team of associated resources are let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of one organization (`tenant` in the json document)\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": ...\n }\n ...\n}\n```\n\nor all organizations\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"*\",\n \"teams\": ...\n }\n ...\n}\n```\n\n"},{"name":"routes.md","id":"/entities/routes.md","url":"/entities/routes.html","title":"Routes","content":"# Routes\n\nA route is an unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins and eventually forward the request to the backend application.\n\n## UI page\n\nYou can find all routes [here](http://otoroshi.oto.tools:8080/bo/dashboard/routes)\n\n## Global Properties\n\n* `location`: the location of the entity\n* `id`: the id of the route\n* `name`: the name of the route\n* `description`: the description of the route\n* `tags`: the tags of the route. can be useful for api automation\n* `metadata`: the metadata of the route. can be useful for api automation. There are a few reserved metadata used by otorshi that can be found @ref[below](./routes.md#reserved-metadata)\n* `enabled`: is the route enabled ? if not, the router will not consider this route\n* `debugFlow`: the debug flag. If enabled, the execution report for this route will contain all input/output values through steps of the proxy engine. For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `capture`: if enabled, otoroshi will generate events containing the whole content of each request. Use with caution ! For more informations, check the @ref[engine documentation](../topics/engine.md#http-traffic-capture)\n* `exportReporting`: if enabled, execution reports of the proxy engine will be generated for each request. Those reports are exportable using @ref[data exporters](./data-exporters.md) . For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `groups`: each route is attached to a group. A group can have one or more services/routes. Each API key is linked to groups/routes/services and allow access to every entities in the groups.\n\n### Reserved metadata\n\nsome metadata are reserved for otoroshi usage. Here is the list of reserved metadata\n\n* `otoroshi-core-user-facing`: is this a user facing app for the snow monkey\n* `otoroshi-core-use-akka-http-client`: use the pure akka http client\n* `otoroshi-core-use-netty-http-client`: use the pure netty http client\n* `otoroshi-core-use-akka-http-ws-client`: use the modern websocket client\n* `otoroshi-core-issue-lets-encrypt-certificate`: enabled let's encrypt certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate`: enabled certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate-ca`: the id of the CA cert to generate the certificate for this route\n* `otoroshi-core-openapi-url`: the openapi url for this route\n* `otoroshi-core-env`: the env for this route. here for legacy reasons\n* `otoroshi-deployment-providers`: in the case of relay routing, the providers for this route\n* `otoroshi-deployment-regions`: in the case of relay routing, the network regions for this route\n* `otoroshi-deployment-zones`: in the case of relay routing, the network zone for this route \n* `otoroshi-deployment-dcs`: in the case of relay routing, the datacenter for this route \n* `otoroshi-deployment-racks`: in the case of relay routing, the rack for this route \n\n## Frontend configuration\n\n* `frontend`: the frontend of the route. It's the configuration that will configure how otoroshi router will match this route. A frontend has the following shape. \n\n```javascript\n{\n \"domains\": [ // the matched domains and paths\n \"new-route.oto.tools/path\" // here you can use wildcard in domain and path, also you can use named path params\n ],\n \"strip_path\": true, // is the matched path stripped in the forwarded request\n \"exact\": false, // perform exact matching on path, if not, will be matched on /path*\n \"headers\": {}, // the matched http headers. if none provided, any header will be matched\n \"query\": {}, // the matched http query params. if none provided, any query params will be matched\n \"methods\": [] // the matched http methods. if none provided, any method will be matched\n}\n```\n\nFor more informations about routing, check the @ref[engine documentation](../topics/engine.md#routing)\n\n## Backend configuration\n\n* `backend`: a backend to forward requests to. For more informations, go to the @ref[backend documentation](./backends.md)\n* `backendRef`: a reference to an existing backend id\n\n## Plugins\n\nthe liste of plugins used on this route. Each plugin definition has the following shape:\n\n```javascript\n{\n \"enabled\": false, // is the plugin enabled\n \"debug\": false, // is debug enabled of this specific plugin\n \"plugin\": \"cp:otoroshi.next.plugins.Redirection\", // the id of the plugin\n \"include\": [], // included paths. if none, all paths are included\n \"exclude\": [], // excluded paths. if none, none paths are excluded\n \"config\": { // the configuration of the plugin\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"plugin_index\": { // the position of the plugin. if none provided, otoroshi will use the order in the plugin array\n \"pre_route\": 0\n }\n}\n```\n\nfor more informations about the available plugins, go @ref[here](../plugins/built-in-plugins.md)\n\n\n"},{"name":"scripts.md","id":"/entities/scripts.md","url":"/entities/scripts.html","title":"Scripts","content":"# Scripts\n\nScript are a way to create plugins for otoroshi without deploying them as jar files. With scripts, you just have to store the scala code of your plugins inside the otoroshi datastore and otoroshi will compile and deploy them at startup. You can find all your scripts in the UI at `cog icon / Plugins`. You can find all the documentation about plugins @ref:[here](../plugins/index.md)\n\n@@@ warning\nThe compilation of your plugins can be pretty long and resources consuming. As the compilation happens during otoroshi boot sequence, your instance will be blocked until all plugins have compiled. This behavior can be disabled. If so, the plugins will not work until they have been compiled. Any service using a plugin that is not compiled yet will fail\n@@@\n\nLike any entity, the script has has the following properties\n\n* `id`\n* `plugin name`\n* `plugin description`\n* `tags`\n* `metadata`\n\nAnd you also have\n\n* `type`: the kind of plugin you are building with this script\n* `plugin code`: the code for your plugin\n\n## Compile\n\nYou can use the compile button to check if the code you write in `plugin code` is valid. It will automatically save your script and try to compile. As mentionned earlier, script compilation is quite resource intensive. It will affect your CPU load and your memory consumption. Don't forget to adjust your VM settings accordingly.\n"},{"name":"service-descriptors.md","id":"/entities/service-descriptors.md","url":"/entities/service-descriptors.html","title":"Service descriptors","content":"# Service descriptors\n\nServices or service descriptor, let you declare how to proxy a call from a domain name to another domain name (or multiple domain names). \n\n@@@ div { .centered-img }\n\n@@@\n\nLet’s say you have an API exposed on http://192.168.0.42 and I want to expose it on https://my.api.foo. Otoroshi will proxy all calls to https://my.api.foo and forward them to http://192.168.0.42. While doing that, it will also log everyhting, control accesses, etc.\n\n\n* `Id`: a unique random string to identify your service\n* `Groups`: each service descriptor is attached to a group. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group.\n* `Create a new group`: you can create a new group to host this descriptor\n* `Create dedicated group`: you can create a new group with an auto generated name to host this descriptor\n* `Name`: the name of your service. Only for debug and human readability purposes.\n* `Description`: the description of your service. Only for debug and human readability purposes.\n* `Service enabled`: activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist.\n* `Read only mode`: authorize only GET, HEAD, OPTIONS calls on this service\n* `Maintenance mode`: display a maintainance page when a user try to use the service\n* `Construction mode`: display a construction page when a user try to use the service\n* `Log analytics`: Log analytics events for this service on the servers\n* `Use new http client`: will use Akka Http Client for every request\n* `Detect apikey asap`: If the service is public and you provide an apikey, otoroshi will detect it and validate it. Of course this setting may impact performances because of useless apikey lookups.\n* `Send Otoroshi headers back`: when enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...\n* `Override Host header`: when enabled, Otoroshi will automatically set the Host header to corresponding target host\n* `Send X-Forwarded-* headers`: when enabled, Otoroshi will send X-Forwarded-* headers to target\n* `Force HTTPS`: will force redirection to `https://` if not present\n* `Allow HTTP/1.0 requests`: will return an error on HTTP/1.0 request\n* `Use new WebSocket client`: will use the new websocket client for every websocket request\n* `TCP/UDP tunneling`: with this setting enabled, otoroshi will not proxy http requests anymore but instead will create a secured tunnel between a cli on your machine and otoroshi to proxy any tcp connection with all otoroshi security features enabled\n\n### Service exposition settings\n\n* `Exposed domain`: the domain used to expose your service. Should follow pattern: `(http|https)://subdomain?.env?.domain.tld?/root?` or regex `(http|https):\\/\\/(.*?)\\.?(.*?)\\.?(.*?)\\.?(.*)\\/?(.*)`\n* `Legacy domain`: use `domain`, `subdomain`, `env` and `matchingRoot` for routing in addition to hosts, or just use hosts.\n* `Strip path`: when matching, strip the matching prefix from the upstream request URL. Defaults to true\n* `Issue Let's Encrypt cert.`: automatically issue and renew let's encrypt certificate based on domain name. Only if Let's Encrypt enabled in global config.\n* `Issue certificate`: automatically issue and renew a certificate based on domain name\n* `Possible hostnames`: all the possible hostnames for your service\n* `Possible matching paths`: all the possible matching paths for your service\n\n### Redirection\n\n* `Redirection enabled`: enabled the redirection. If enabled, a call to that service will redirect to the chosen URL\n* `Http redirection code`: type of redirection used\n* `Redirect to`: URL used to redirect user when the service is called\n\n### Service targets\n\n* `Redirect to local`: if you work locally with Otoroshi, you may want to use that feature to redirect one specific service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests\n* `Load balancing`: the load balancing algorithm used\n* `Targets`: the list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n* `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is `/api/foo`, then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n\n### URL Patterns\n\n* `Make service a 'public ui'`: add a default pattern as public routes\n* `Make service a 'private api'`: add a default pattern as private routes\n* `Public patterns`: by default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use `/.*`\n* `Private patterns`: if you define a public pattern that is a little bit too much, you can make some of public URL private again\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Otoroshi exchange protocol\n\n* `Enabled`: when enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside.\n* `Send challenge`: when disbaled, Otoroshi will not check if target service respond with sent random value.\n* `Send info. token`: when enabled, Otoroshi add an additional header containing current call informations\n* `Challenge token version`: version the otoroshi exchange protocol challenge. This option will be set to V2 in a near future.\n* `Info. token version`: version the otoroshi exchange protocol info token. This option will be set to Latest in a near future.\n* `Tokens TTL`: the number of seconds for tokens (state and info) lifes\n* `State token header name`: the name of the header containing the state token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.state)\n* `State token response header name`: the name of the header containing the state response token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.stateresp)\n* `Info token header name`: the name of the header containing the info token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.claim)\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Use same algo.`: when enabled, all JWT token in this section will use the same signing algorithm. If `use same algo.` is disabled, three more options will be displayed to select an algorithm for each step of the calls :\n * Otoroshi to backend\n * Backend to otoroshi\n * Info. token\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n### Authentication\n\n* `Enforce user authentication`: when enabled, user will be allowed to use the service (UI) only if they are registered users of the chosen authentication module.\n* `Auth. config`: authentication module used to protect the service\n* `Create a new auth config.`: navigate to the creation of authentication module page\n* `all auth config.`: navigate to the authentication pages\n\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Strict mode`: strict mode enabled\n\n### Api keys constraints\n\n* `From basic auth.`: you can pass the api key in Authorization header (ie. from 'Authorization: Basic xxx' header)\n* `Allow client id only usage`: you can pass the api key using client id only (ie. from Otoroshi-Token header)\n* `From custom headers`: you can pass the api key using custom headers (ie. Otoroshi-Client-Id and Otoroshi-Client-Secret headers)\n* `From JWT token`: you can pass the api key using a JWT token (ie. from 'Authorization: Bearer xxx' header)\n\n#### Basic auth. Api Key\n\n* `Custom header name`: the name of the header to get Authorization\n* `Custom query param name`: the name of the query param to get Authorization\n\n#### Client ID only Api Key\n\n* `Custom header name`: the name of the header to get the client id\n* `Custom query param name`: the name of the query param to get the client id\n\n#### Custom headers Api Key\n\n* `Custom client id header name`: the name of the header to get the client id\n* `Custom client secret header name`: the name of the header to get the client secret\n\n#### JWT Token Api Key\n\n* `Secret signed`: JWT can be signed by apikey secret using HMAC algo.\n* `Keypair signed`: JWT can be signed by an otoroshi managed keypair using RSA/EC algo.\n* `Include Http request attrs.`: if enabled, you have to put the following fields in the JWT token corresponding to the current http call (httpPath, httpVerb, httpHost)\n* `Max accepted token lifetime`: the maximum number of second accepted as token lifespan\n* `Custom header name`: the name of the header to get the jwt token\n* `Custom query param name`: the name of the query param to get the jwt token\n* `Custom cookie name`: the name of the cookie to get the jwt token\n\n### Routing constraints\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata\n\n### CORS support\n\n* `Enabled`: if enabled, CORS header will be check for each incoming request\n* `Allow credentials`: if enabled, the credentials will be sent. Credentials are cookies, authorization headers, or TLS client certificates.\n* `Allow origin`: if enabled, it will indicates whether the response can be shared with requesting code from the given\n* `Max age`: response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.\n* `Expose headers`: response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.\n* `Allow headers`: response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.\n* `Allow methods`: response header specifies one or more methods allowed when accessing a resource in response to a preflight request.\n* `Excluded patterns`: by default, when cors is enabled, everything has cors. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n#### Related documentations\n\n* @link[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) { open=new }\n* @link[Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) { open=new }\n* @link[Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) { open=new }\n* @link[Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) { open=new }\n* @link[Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) { open=new }\n\n### JWT tokens verification\n\n* `Verifiers`: list of selected verifiers to apply on the service\n* `Enabled`: if enabled, Otoroshi will enabled each verifier of the previous list\n* `Excluded patterns`: list of routes where the verifiers will not be apply\n\n### Pre Routing\n\nThis part has been deprecated and moved to the plugin section.\n\n### Access validation\nThis part has been deprecated and moved to the plugin section.\n\n### Gzip support\n\n* `Mimetypes allowed list`: gzip only the files that are matching to a format in the list\n* `Mimetypes blocklist`: will not gzip files matching to a format in the list. A possible way is to allowed all format by default by setting a `*` on the `Mimetypes allowed list` and to add the unwanted format in this list.\n* `Compression level`: the compression level where 9 gives us maximum compression but at the slowest speed. The default compression level is 5 and is a good compromise between speed and compression ratio.\n* `Buffer size`: chunking up a stream of bytes into limited size\n* `Chunk threshold`: if the content type of a request reached over the threshold, the response will be chunked\n* `Excluded patterns`: by default, when gzip is enabled, everything has gzip. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n### Client settings\n\n* `Use circuit breaker`: use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !\n* `Cache connections`: use a cache at host connection level to avoid reconnection time\n* `Client attempts`: specify how many times the client will retry to fetch the result of the request after an error before giving up.\n* `Client call timeout`: specify how long each call should last at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n* `C.breaker max errors`: specify how many errors can pass before opening the circuit breaker\n* `C.breaker retry delay`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor\n* `C.breaker backoff factor`: specify the factor to multiply the delay for each retry\n* `C.breaker window`: specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted\n\n#### Custom timeout settings (list)\n\n* `Path`: the path on which the timeout will be active\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Call timeout`: Specify how long each call should last at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n#### Proxy settings\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n### HTTP Headers\n\n* `Additional Headers In`: specify headers that will be added to each client request (from Otoroshi to target). Useful to add authentication.\n* `Additional Headers Out`: specify headers that will be added to each client response (from Otoroshi to client).\n* `Missing only Headers In`: specify headers that will be added to each client request (from Otoroshi to target) if not in the original request.\n* `Missing only Headers Out`: specify headers that will be added to each client response (from Otoroshi to client) if not in the original response.\n* `Remove incoming headers`: remove headers in the client request (from client to Otoroshi).\n* `Remove outgoing headers`: remove headers in the client response (from Otoroshi to client).\n* `Security headers`:\n* `Utility headers`:\n* `Matching Headers`: specify headers that MUST be present on client request to route it (pre routing). Useful to implement versioning.\n* `Headers verification`: verify that some headers has a specific value (post routing)\n\n### Additional settings \n\n* `OpenAPI`: specify an open API descriptor. Useful to display the documentation\n* `Tags`: specify tags for the service\n* `Metadata`: specify metadata for the service. Useful for analytics\n* `IP allowed list`: IP address that can access the service\n* `IP blocklist`: IP address that cannot access the service\n\n### Canary mode\n\n* `Enabled`: Canary mode enabled\n* `Traffic split`: Ratio of traffic that will be sent to canary targets. For instance, if traffic is at 0.2, for 10 request, 2 request will go on canary targets and 8 will go on regular targets.\n* `Targets`: The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n * `Target`:\n * `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n* `Campaign stats`:\n* `Use canary targets as standard targets`:\n\n### Healthcheck settings\n\n* `HealthCheck enabled`: to help failing fast, you can activate healthcheck on a specific URL.\n* `HealthCheck url`: the URL to check. Should return an HTTP 200 response. You can also respond with an 'Opun-Health-Check-Logic-Test-Result' header set to the value of the 'Opun-Health-Check-Logic-Test' request header + 42. to make the healthcheck complete.\n\n### Fault injection\n\n* `User facing app.`: if service is set as user facing, Snow Monkey can be configured to not being allowed to create outage on them.\n* `Chaos enabled`: activate or deactivate chaos setting on this service descriptor.\n\n### Custom errors template\n\n* `40x template`: html template displayed when 40x error occurred\n* `50x template`: html template displayed when 50x error occurred\n* `Build mode template`: html template displayed when the build mode is enabled\n* `Maintenance mode template`: html template displayed when the maintenance mode is enabled\n* `Custom messages`: override error message one by one\n\n### Request transformation\n\nThis part has been deprecated and moved to the plugin section.\n\n### Plugins\n\n* `Plugins`:\n \n * `Inject default config`: injects, if present, the default configuration of a selected plugin in the configuration object\n * `Documentation`: link to the documentation website of the plugin\n * `show/hide config. panel`: shows and hides the plugin panel which contains the plugin description and configuration\n* `Excluded patterns`: by default, when plugins are enabled, everything pass in. But sometimes you need to exclude something, so just add regex to matching path you want to exlude.\n* `Configuration`: the configuration of each enabled plugin, split by names and grouped in the same configuration object."},{"name":"service-groups.md","id":"/entities/service-groups.md","url":"/entities/service-groups.html","title":"Service groups","content":"# Service groups\n\nA service group is composed of an unique `id`, a `Group name`, a `Group description`, an `Organization` and a `Team`. As all Otoroshi resources, a service group have a list of tags and metadata associated.\n\n@@@ div { .centered-img }\n\n@@@\n\nThe first instinctive usage of service group is to group a list of services. \n\nWhen it's done, you can authorize an api key on a specific group. Instead of authorize an api key for each service, you can regroup a list of services together, and give authorization on the group (read the page on the api keys and the usage of the `Authorized on.` field).\n\n## Access to the list of service groups\n\nTo visualize and edit the list of groups, you can navigate to your instance on the `https://otoroshi.xxxxx/bo/dashboard/groups` route or click on the cog icon and select the Service groups button.\n\nOnce on the page, you can create a new item, edit an existing service group or delete an existing one.\n\n> When a service group is deleted, the resources associated are not deleted. On the other hand, the service group of associated resources is let empty.\n\n"},{"name":"tcp-services.md","id":"/entities/tcp-services.md","url":"/entities/tcp-services.html","title":"TCP services","content":"# TCP services\n\nTCP service are special kind of otoroshi services meant to proxy pure TCP connections (ssh, database, http, etc)\n\n## Global information\n\n* `Id`: generated unique identifier\n* `TCP service name`: the name of your TCP service\n* `Enabled`: enable and disable the service\n* `TCP service port`: the listening port\n* `TCP service interface`: network interface listen by the service\n* `Tags`: list of tags associated to the service\n* `Metadata`: list of metadata associated to the service\n\n## TLS\n\nthis section controls the TLS exposition of the service\n\n* `TLS mode`\n * `Disabled`: no TLS\n * `PassThrough`: as the target exposes TLS, the call will pass through otoroshi and use target TLS\n * `Enabled`: the service will be exposed using TLS and will chose certificate based on SNI\n* `Client Auth.`\n * `None` no mTLS needed to pass\n * `Want` pass with or without mTLS\n * `Need` need mTLS to pass\n\n## Server Name Indication (SNI)\n\nthis section control how SNI should be treated\n\n* `SNI routing enabled`: if enabled, the server will use the SNI hostname to determine which certificate to present to the client\n* `Forward to target if no SNI match`: if enabled, a call without any SNI match will be forward to the target\n* `Target host`: host of the target called if no SNI\n* `Target ip address`: ip of the target called if no SNI\n* `Target port`: port of the target called if no SNI\n* `TLS call`: encrypt the communication with TLS\n\n## Rules\n\nfor any listening TCP proxy, it is possible to route to multiple targets based on SNI or extracted http host (if proxying http)\n\n* `Matching domain name`: regex used to filter the list of domains where the rule will be applied\n* `Target host`: host of the target\n* `Target ip address`: ip of the target\n* `Target port`: port of the target\n* `TLS call`: enable this flag if the target is exposed using TLS\n"},{"name":"teams.md","id":"/entities/teams.md","url":"/entities/teams.html","title":"Teams","content":"# Teams\n\nIn Otoroshi, all resources are attached to an `Organization` and a `Team`. \n\nA team is composed of an unique `id`, a `name`, a `description` and an `Organization`. As all Otoroshi resources, a Team have a list of tags and metadata associated.\n\nA team have an unique organization and can be use on multiples resources (services, api keys, etc ...).\n\nA connected user on Otoroshi UI has a list of teams and organizations associated. It can be helpful when you want restrict the rights of a connected user.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of teams\n\nTo visualize and edit the list of teams, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/teams` route or click on the cog icon and select the teams button.\n\nOnce on the page, you can create a new item, edit an existing team or delete an existing one.\n\n> When a team is deleted, the resources associated are not deleted. On the other hand, the team of associated resources is let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of multiple teams in an organization\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"team-1\",\n \"team-2\"\n ]\n }\n ...\n}\n```\n\nor all teams\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"*\"\n ]\n }\n ...\n}\n```"},{"name":"features.md","id":"/features.md","url":"/features.html","title":"Features","content":"# Features\n\n**Traffic Management**\n\n* Can proxy any HTTP(s) service (apis, webapps, websocket, etc)\n* Can proxy any TCP service (app, database, etc)\n* Can proxy any GRPC service\n* Multiple load-balancing options: \n * RoundRobin\n * Random, Sticky\n * Ip address hash\n * Best Response Time\n* Distributed in-flight request limiting\t\n* Distributed rate limiting \n* End-to-end HTTP/1.1 support\n* End-to-end H2 support\n* End-to-end H3 support\n* Traffic mirroring\n* Traffic capture\n* Canary deployments\n* Relay routing \n* Tunnels for easier network exposition\n* Error templates\n\n**Routing**\n\n* Router can support ten of thousands of concurrent routes\n* Router support path params extraction (can be regex validated)\n* Routing based on \n * method\n * hostname (exact, wildcard)\n * path (exact, wildcard)\n * header values (exact, regex, wildcard)\n * query param values (exact, regex, wildcard)\n* Support full url rewriting\n\n**Routes customization**\n\n* Dozens of built-in middlewares (policies/plugins) \n * circuit breakers\n * automatic retries\n * buffering\n * gzip\n * headers manipulation\n * cors\n * body transformation\n * graphql gateway\n * etc \n* Support middlewares compiled to WASM (using extism)\n* Support Open Policy Agent policies for traffic control\n* Write your own custom middlewares\n * in scala deployed as jar files\n * in whatever language you want that can be compiled to WASM\n\n**Routes Monitoring**\n\n* Active healthchecks\n* Route state for the last 90 days\n* Calls tracing using W3C trace context\n* Export alerts and events to external database\n * file\n * S3\n * elastic\n * pulsar\n * kafka\n * webhook\n * mailer\n * logger\n* Real-time traffic metrics\n* Real-time traffic metrics (Datadog, Prometheus, StatsD)\n\n**Services discovery**\n\n* through DNS\n* through Eureka 2\n* through Kubernetes API\n* through custom otoroshi protocol\n\n**API security**\n\n* Access management with apikeys and quotas\n* Automatic apikeys secrets rotation\n* HTTPS and TLS\n* End-to-end mTLS calls \n* Routing constraints\n* Routing restrictions\n* JWT tokens validation and manipulation\n * can support multiple validator on the same routes\n\n**Administration UI**\n\n* Manage and organize all resources\n* Secured users access with Authentication module\n* Audited users actions\n* Dynamic changes at runtime without full reload\n* Test your routes without any external tools\n\n**Webapp authentication and security**\n\n* OAuth2.0/2.1 authentication\n* OpenID Connect (OIDC) authentication\n* LDAP authentication\n* JWT authentication\n* OAuth 1.0a authentication\n* SAML V2 authentication\n* Internal users management\n* Secret vaults support\n * Environment variables\n * Hashicorp Vault\n * Azure key vault\n * AWS secret manager\n * Google secret manager\n * Kubernetes secrets\n * Izanami\n * Spring Cloud Config\n * Http\n * Local\n\n**Certificates management**\n\n* Dynamic TLS certificates store \n* Dynamic TLS termination\n* Internal PKI\n * generate self signed certificates/CAs\n * generate/sign certificates/CAs/subCAs\n * AIA\n * OCSP responder\n * import P12/certificate bundles\n* ACME / Let's Encrypt support\n* On-the-fly certificate generation based on a CA certificate without request loss\n* JWKS exposition for public keypair\n* Default certificate\n* Customize mTLS trusted CAs in the TLS handshake\n\n**Clustering**\n\n* based on a control plane/data plane pattern\n* encrypted communication\n* backup capabilities to allow data plane to start without control plane reachable to improve resilience\n* relay routing to forward traffic from one network zone to others\n* distributed web authentication accross nodes\n\n**Performances and testing**\n\n* Chaos engineering\n* Horizontal Scalability or clustering\n* Canary testing\n* Http client in UI\n* Request debugging\n* Traffic capture\n\n**Kubernetes integration**\n\n* Standard Ingress controller\n* Custom Ingress controller\n * Manage Otoroshi resources from Kubernetes\n* Validation of resources via webhook\n* Service Mesh for easy service-to-service communication (based on Kubernetes sidecars)\n\n**Organize**\n\n* multi-organizations\n* multi-teams\n* routes groups\n\n**Developpers portal**\n\n* Using @link:[Daikoku](https://maif.github.io/daikoku/manual/index.html) { open=new }\n"},{"name":"getting-started.md","id":"/getting-started.md","url":"/getting-started.html","title":"Getting Started","content":"# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
      \nRoute plugins:\nApikeys\n
      \n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
      \nRoute plugins:\nAuthentication\n
      \n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial."},{"name":"communicate-with-kafka.md","id":"/how-to-s/communicate-with-kafka.md","url":"/how-to-s/communicate-with-kafka.html","title":"Communicate with Kafka","content":"# Communicate with Kafka\n\nEvery matching event can be sent to an [Apache Kafka topic](https://kafka.apache.org/).\n\n### SASL mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:SASL_PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n - KAFKA_CLIENT_USERS=user\n - KAFKA_CLIENT_PASSWORDS=password\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### PLAINTEXT mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SSL mechanism\n\n````bash\nwget https://raw.githubusercontent.com/confluentinc/confluent-platform-security-tools/master/kafka-generate-ssl.sh\n````\n\n````bash\nchmod +x kafka-generate-ssl.sh\n````\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"wurstmeister/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n\n kafka:\n image: wurstmeister/kafka:2.12-2.2.0\n depends_on:\n - zookeeper\n ports:\n - \"9092:9092\"\n environment:\n KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:9092'\n KAFKA_LISTENERS: 'SSL://0.0.0.0:9092'\n KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'\n KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_SSL_KEYSTORE_LOCATION: '/keystore/kafka.keystore.jks'\n KAFKA_SSL_KEYSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_KEY_PASSWORD: 'otoroshi'\n KAFKA_SSL_TRUSTSTORE_LOCATION: '/truststore/kafka.truststore.jks'\n KAFKA_SSL_TRUSTSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_SECURITY_INTER_BROKER_PROTOCOL: 'SSL'\n volumes:\n - ./truststore:/truststore\n - ./keystore:/keystore\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SASL_SSL mechanism\n\nGenerate the TLS certificates for the Kafka broker.\n\nCreate a file `generate.sh` with the following content and run the command\n\n````bash\nchmod +x generate.sh && ./generate.sh\n````\n\n````bash\n# Content of the generate.sh file\n\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚°\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚° cat generate.sh\n#!/usr/bin/env bash\n\nset -e\n\nKEYSTORE_FILENAME=\"kafka.keystore.jks\"\nVALIDITY_IN_DAYS=3650\nDEFAULT_TRUSTSTORE_FILENAME=\"kafka.truststore.jks\"\nTRUSTSTORE_WORKING_DIRECTORY=\"truststore\"\nKEYSTORE_WORKING_DIRECTORY=\"keystore\"\nCA_CERT_FILE=\"ca-cert\"\nKEYSTORE_SIGN_REQUEST=\"cert-file\"\nKEYSTORE_SIGN_REQUEST_SRL=\"ca-cert.srl\"\nKEYSTORE_SIGNED_CERT=\"cert-signed\"\n\nfunction file_exists_and_exit() {\n echo \"'$1' cannot exist. Move or delete it before\"\n echo \"re-running this script.\"\n exit 1\n}\n\nif [ -e \"$KEYSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $KEYSTORE_WORKING_DIRECTORY\nfi\n\nif [ -e \"$CA_CERT_FILE\" ]; then\n file_exists_and_exit $CA_CERT_FILE\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST_SRL\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST_SRL\nfi\n\nif [ -e \"$KEYSTORE_SIGNED_CERT\" ]; then\n file_exists_and_exit $KEYSTORE_SIGNED_CERT\nfi\n\necho\necho \"Welcome to the Kafka SSL keystore and truststore generator script.\"\n\necho\necho \"First, do you need to generate a trust store and associated private key,\"\necho \"or do you already have a trust store file and private key?\"\necho\necho -n \"Do you need to generate a trust store and associated private key? [yn] \"\nread generate_trust_store\n\ntrust_store_file=\"\"\ntrust_store_private_key_file=\"\"\n\nif [ \"$generate_trust_store\" == \"y\" ]; then\n if [ -e \"$TRUSTSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $TRUSTSTORE_WORKING_DIRECTORY\n fi\n\n mkdir $TRUSTSTORE_WORKING_DIRECTORY\n echo\n echo \"OK, we'll generate a trust store and associated private key.\"\n echo\n echo \"First, the private key.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - A password for the private key. Remember this.\"\n echo \" - Information about you and your company.\"\n echo \" - NOTE that the Common Name (CN) is currently not important.\"\n\n openssl req -new -x509 -keyout $TRUSTSTORE_WORKING_DIRECTORY/ca-key \\\n -out $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -days $VALIDITY_IN_DAYS\n\n trust_store_private_key_file=\"$TRUSTSTORE_WORKING_DIRECTORY/ca-key\"\n\n echo\n echo \"Two files were created:\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-key -- the private key used later to\"\n echo \" sign certificates\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -- the certificate that will be\"\n echo \" stored in the trust store in a moment and serve as the certificate\"\n echo \" authority (CA). Once this certificate has been stored in the trust\"\n echo \" store, it will be deleted. It can be retrieved from the trust store via:\"\n echo \" $ keytool -keystore -export -alias CARoot -rfc\"\n\n echo\n echo \"Now the trust store will be generated from the certificate.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - the trust store's password (labeled 'keystore'). Remember this\"\n echo \" - a confirmation that you want to import the certificate\"\n\n keytool -keystore $TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME \\\n -alias CARoot -import -file $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\n\n trust_store_file=\"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME\"\n\n echo\n echo \"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME was created.\"\n\n # don't need the cert because it's in the trust store.\n rm $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\nelse\n echo\n echo -n \"Enter the path of the trust store file. \"\n read -e trust_store_file\n\n if ! [ -f $trust_store_file ]; then\n echo \"$trust_store_file isn't a file. Exiting.\"\n exit 1\n fi\n\n echo -n \"Enter the path of the trust store's private key. \"\n read -e trust_store_private_key_file\n\n if ! [ -f $trust_store_private_key_file ]; then\n echo \"$trust_store_private_key_file isn't a file. Exiting.\"\n exit 1\n fi\nfi\n\necho\necho \"Continuing with:\"\necho \" - trust store file: $trust_store_file\"\necho \" - trust store private key: $trust_store_private_key_file\"\n\nmkdir $KEYSTORE_WORKING_DIRECTORY\n\necho\necho \"Now, a keystore will be generated. Each broker and logical client needs its own\"\necho \"keystore. This script will create only one keystore. Run this script multiple\"\necho \"times for multiple keystores.\"\necho\necho \"You will be prompted for the following:\"\necho \" - A keystore password. Remember it.\"\necho \" - Personal information, such as your name.\"\necho \" NOTE: currently in Kafka, the Common Name (CN) does not need to be the FQDN of\"\necho \" this host. However, at some point, this may change. As such, make the CN\"\necho \" the FQDN. Some operating systems call the CN prompt 'first / last name'\"\necho \" - A key password, for the key being generated within the keystore. Remember this.\"\n\n# To learn more about CNs and FQDNs, read:\n# https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedTrustManager.html\n\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME \\\n -alias localhost -validity $VALIDITY_IN_DAYS -genkey -keyalg RSA\n\necho\necho \"'$KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME' now contains a key pair and a\"\necho \"self-signed certificate. Again, this keystore can only be used for one broker or\"\necho \"one logical client. Other brokers or clients need to generate their own keystores.\"\n\necho\necho \"Fetching the certificate from the trust store and storing in $CA_CERT_FILE.\"\necho\necho \"You will be prompted for the trust store's password (labeled 'keystore')\"\n\nkeytool -keystore $trust_store_file -export -alias CARoot -rfc -file $CA_CERT_FILE\n\necho\necho \"Now a certificate signing request will be made to the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost \\\n -certreq -file $KEYSTORE_SIGN_REQUEST\n\necho\necho \"Now the trust store's private key (CA) will sign the keystore's certificate.\"\necho\necho \"You will be prompted for the trust store's private key password.\"\nopenssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \\\n -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \\\n -days $VALIDITY_IN_DAYS -CAcreateserial\n# creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed.\n\necho\necho \"Now the CA will be imported into the keystore.\"\necho\necho \"You will be prompted for the keystore's password and a confirmation that you want to\"\necho \"import the certificate.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias CARoot \\\n -import -file $CA_CERT_FILE\nrm $CA_CERT_FILE # delete the trust store cert because it's stored in the trust store.\n\necho\necho \"Now the keystore's signed certificate will be imported back into the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost -import \\\n -file $KEYSTORE_SIGNED_CERT\n\necho\necho \"All done!\"\necho\necho \"Delete intermediate files? They are:\"\necho \" - '$KEYSTORE_SIGN_REQUEST_SRL': CA serial number\"\necho \" - '$KEYSTORE_SIGN_REQUEST': the keystore's certificate signing request\"\necho \" (that was fulfilled)\"\necho \" - '$KEYSTORE_SIGNED_CERT': the keystore's certificate, signed by the CA, and stored back\"\necho \" into the keystore\"\necho -n \"Delete? [yn] \"\nread delete_intermediate_files\n\nif [ \"$delete_intermediate_files\" == \"y\" ]; then\n rm $KEYSTORE_SIGN_REQUEST_SRL\n rm $KEYSTORE_SIGN_REQUEST\n rm $KEYSTORE_SIGNED_CERT\nfi\n````\n\nCreate, in the same repository, a repository named `secrets` with the following configuration.\n\n````bash \n# Content of ~/tmp/kafka/secrets/kafka_server_jaas.conf\n\nClient {\n org.apache.kafka.common.security.plain.PlainLoginModule required\n username=\"user\"\n password=\"password\";\n};\n````\n\nCreate a `docker-compose.yml` file with the following content.\n\n````bash\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n````\n\nAt this point, your repository should be \n````\n/tmp/kafka\n | generate.sh\n | docker-compose.yml\n | truststore\n | kafka.truststore.jks\n | keystore \n | kafka.keystore.jks\n | secrets \n | kafka_server_jaas.conf\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"create-custom-auth-module.md","id":"/how-to-s/create-custom-auth-module.md","url":"/how-to-s/create-custom-auth-module.html","title":"Create your Authentication module","content":"# Create your Authentication module\n\nAuthentication modules can be used to protect routes. In some cases, you need to create your own custom authentication module to create a new one or simply inherit and extend an exiting module.\n\nYou can write your own authentication using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"my-custom-auth-module\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1x.x.x\"\n )\n```\n\nJust below, you can find an example of Custom Auth. module. \n\n```scala\npackage auth.custom\n\nimport akka.http.scaladsl.util.FastFuture\nimport otoroshi.auth.{AuthModule, AuthModuleConfig, Form, SessionCookieValues}\nimport otoroshi.controllers.routes\nimport otoroshi.env.Env\nimport otoroshi.models._\nimport otoroshi.security.IdGenerator\nimport otoroshi.utils.JsonPathValidator\nimport otoroshi.utils.syntax.implicits.BetterSyntax\nimport play.api.http.MimeTypes\nimport play.api.libs.json._\nimport play.api.mvc._\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\ncase class CustomModuleConfig(\n id: String,\n name: String,\n desc: String,\n clientSideSessionEnabled: Boolean,\n sessionMaxAge: Int = 86400,\n userValidators: Seq[JsonPathValidator] = Seq.empty,\n tags: Seq[String],\n metadata: Map[String, String],\n sessionCookieValues: SessionCookieValues,\n location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),\n form: Option[Form] = None,\n foo: String = \"bar\"\n ) extends AuthModuleConfig {\n def `type`: String = \"custom\"\n def humanName: String = \"Custom Authentication\"\n\n override def authModule(config: GlobalConfig): AuthModule = CustomAuthModule(this)\n override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)\n\n lazy val format = new Format[CustomModuleConfig] {\n override def writes(o: CustomModuleConfig): JsValue = o.asJson\n\n override def reads(json: JsValue): JsResult[CustomModuleConfig] = Try {\n CustomModuleConfig(\n location = otoroshi.models.EntityLocation.readFromKey(json),\n id = (json \\ \"id\").as[String],\n name = (json \\ \"name\").as[String],\n desc = (json \\ \"desc\").asOpt[String].getOrElse(\"--\"),\n clientSideSessionEnabled = (json \\ \"clientSideSessionEnabled\").asOpt[Boolean].getOrElse(true),\n sessionMaxAge = (json \\ \"sessionMaxAge\").asOpt[Int].getOrElse(86400),\n metadata = (json \\ \"metadata\").asOpt[Map[String, String]].getOrElse(Map.empty),\n tags = (json \\ \"tags\").asOpt[Seq[String]].getOrElse(Seq.empty[String]),\n sessionCookieValues =\n (json \\ \"sessionCookieValues\").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),\n userValidators = (json \\ \"userValidators\")\n .asOpt[Seq[JsValue]]\n .map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))\n .getOrElse(Seq.empty),\n form = (json \\ \"form\").asOpt[JsValue].flatMap(json => Form._fmt.reads(json) match {\n case JsSuccess(value, _) => Some(value)\n case JsError(_) => None\n }),\n foo = (json \\ \"foo\").asOpt[String].getOrElse(\"bar\")\n )\n } match {\n case Failure(exception) => JsError(exception.getMessage)\n case Success(value) => JsSuccess(value)\n }\n }.asInstanceOf[Format[AuthModuleConfig]]\n\n override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = format\n\n override def asJson =\n location.jsonWithKey ++ Json.obj(\n \"type\" -> \"custom\",\n \"id\" -> this.id,\n \"name\" -> this.name,\n \"desc\" -> this.desc,\n \"clientSideSessionEnabled\" -> this.clientSideSessionEnabled,\n \"sessionMaxAge\" -> this.sessionMaxAge,\n \"metadata\" -> this.metadata,\n \"tags\" -> JsArray(tags.map(JsString.apply)),\n \"sessionCookieValues\" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),\n \"userValidators\" -> JsArray(userValidators.map(_.json)),\n \"form\" -> this.form.map(Form._fmt.writes),\n \"foo\" -> foo\n )\n\n def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)\n\n override def cookieSuffix(desc: ServiceDescriptor) = s\"custom-auth-$id\"\n def theDescription: String = desc\n def theMetadata: Map[String, String] = metadata\n def theName: String = name\n def theTags: Seq[String] = tags\n}\n\nobject CustomAuthModule {\n def defaultConfig = CustomModuleConfig(\n id = IdGenerator.namedId(\"auth_mod\", IdGenerator.uuid),\n name = \"My custom auth. module\",\n desc = \"My custom auth. module\",\n tags = Seq.empty,\n metadata = Map.empty,\n sessionCookieValues = SessionCookieValues(),\n clientSideSessionEnabled = true,\n form = None)\n}\n\ncase class CustomAuthModule(authConfig: CustomModuleConfig) extends AuthModule {\n def this() = this(CustomAuthModule.defaultConfig)\n\n override def paLoginPage(request: RequestHeader, config: GlobalConfig, descriptor: ServiceDescriptor, isRoute: Boolean)\n (implicit ec: ExecutionContext, env: Env): Future[Result] = {\n val redirect = request.getQueryString(\"redirect\")\n val hash = env.sign(s\"${authConfig.id}:::${descriptor.id}\")\n env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>\n Results\n .Ok(auth.custom.views.html.login(s\"/privateapps/generic/callback?desc=${descriptor.id}&hash=$hash&route=${isRoute}\", token))\n .as(MimeTypes.HTML)\n .addingToSession(\n \"ref\" -> authConfig.id,\n s\"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}\" -> redirect.getOrElse(\n routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)(request)\n )\n )(request)\n .future\n }\n }\n\n override def paLogout(request: RequestHeader, user: Option[PrivateAppsUser], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = FastFuture.successful(Right(None))\n\n override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[String, PrivateAppsUser]] = {\n PrivateAppsUser(\n randomId = IdGenerator.token(64),\n name = \"foo\",\n email = s\"foo@oto.tools\",\n profile = Json.obj(\n \"name\" -> \"foo\",\n \"email\" -> s\"foo@oto.tools\"\n ),\n realm = authConfig.cookieSuffix(descriptor),\n otoroshiData = None,\n authConfigId = authConfig.id,\n tags = Seq.empty,\n metadata = Map.empty,\n location = authConfig.location\n )\n .validate(authConfig.userValidators)\n .vfuture\n }\n\n override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Result] = ???\n\n override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = ???\n\n override def boCallback(request: Request[AnyContent], config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[String, BackOfficeUser]] = ???\n}\n```\n\nThis custom Auth. module inherits from AuthModule (the Auth module have to inherit from the AuthModule trait to be found by Otoroshi). It exposes a simple UI to login, and create an user for each callback request without any verification. Methods starting with bo will be called in case that the auth. module is used on the back office and in other cases, the pa methods (pa for Private App) will be called to protect a route.\n\nThis custom Auth. module uses a [Play template](https://www.playframework.com/documentation/2.8.x/ScalaTemplates) to display the login page. It's not required by Otoroshi but it's a easy way to create a login form.\n\n```html \n@import otoroshi.env.Env\n\n@(action: String, token: String)\n\n
      \n

      Login page

      \n\n
      \n \n \n Login\n \n \n
      \n```\n\nYour hierarchy files should be something like:\n\n```\nauth\n| custom\n |customModule.scala\n | views\n | login.scala.html\n```\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/customModule.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in the authentication modules, you can chose your custom module in the list."},{"name":"custom-initial-state.md","id":"/how-to-s/custom-initial-state.md","url":"/how-to-s/custom-initial-state.html","title":"Initial state customization","content":"# Initial state customization\n\nwhen you start otoroshi for the first time, some basic entities will be created and stored in the datastore in order to make your instance work properly. However it might not be enough for your use case but you do want to bother with restoring a complete otoroshi export.\n\nIn order to make state customization easy, otoroshi provides the config. key `otoroshi.initialCustomization`, overriden by the env. variable `OTOROSHI_INITIAL_CUSTOMIZATION`\n\nThe expected structure is the following :\n\n```javascript\n{\n \"config\": { ... },\n \"admins\": [],\n \"simpleAdmins\": [],\n \"serviceGroups\": [],\n \"apiKeys\": [],\n \"serviceDescriptors\": [],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [],\n \"teams\": []\n}\n```\n\nin this structure, everything is optional. For every array property, items will be added to the datastore. For the global config. object, you can just add the parts that you need, and they will be merged with the existing config. object of the datastore.\n\n## Customize the global config.\n\nfor instance, if you want to customize the behavior of the TLS termination, you can use the following :\n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"config\":{\"tlsSettings\":{\"defaultDomain\":\"www.foo.bar\",\"randomIfNotFound\":false}}'\n```\n\n## Customize entities\n\nif you want to add apikeys at first boot \n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"apikeys\":[{\"_loc\":{\"tenant\":\"default\",\"teams\":[\"default\"]},\"clientId\":\"ksVlQ2KlZm0CnDfP\",\"clientSecret\":\"usZYbE1iwSsbpKY45W8kdbZySj1M5CWvFXe0sPbZ0glw6JalMsgorDvSBdr2ZVBk\",\"clientName\":\"awesome-apikey\",\"description\":\"the awesome apikey\",\"authorizedGroup\":\"default\",\"authorizedEntities\":[\"group_default\"],\"enabled\":true,\"readOnly\":false,\"allowClientIdOnly\":false,\"throttlingQuota\":10000000,\"dailyQuota\":10000000,\"monthlyQuota\":10000000,\"constrainedServicesOnly\":false,\"restrictions\":{\"enabled\":false,\"allowLast\":true,\"allowed\":[],\"forbidden\":[],\"notFound\":[]},\"rotation\":{\"enabled\":false,\"rotationEvery\":744,\"gracePeriod\":168,\"nextSecret\":null},\"validUntil\":null,\"tags\":[],\"metadata\":{}}]}'\n```\n"},{"name":"custom-log-levels.md","id":"/how-to-s/custom-log-levels.md","url":"/how-to-s/custom-log-levels.html","title":"Log levels customization","content":"# Log levels customization\n\nIf you want to customize the log level of your otoroshi instances, it's pretty easy to do it using environment variables or configuration file.\n\n## Customize log level for one logger with configuration file\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare in your otoroshi configuration file\n\n```\notoroshi.loggers {\n ...\n otoroshi-http-handler = \"DEBUG\"\n ...\n}\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## Customize log level for one logger with environment variable\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare an environment variable named `OTOROSHI_LOGGERS_OTOROSHI_HTTP_HANDLER` with value `DEBUG`. The rule is \n\n```scala\n\"OTOROSHI_LOGGERS_\" + loggerName.toUpperCase().replace(\"-\", \"_\")\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## List of loggers\n\n* [`otoroshi-error-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-handler%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-http-handler-debug`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler-debug%22%29)\n* [`otoroshi-websocket-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler%22%29)\n* [`otoroshi-websocket`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket%22%29)\n* [`otoroshi-websocket-handler-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler-actor%22%29)\n* [`otoroshi-snowmonkey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-worker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-worker%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-auth-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-controller%22%29)\n* [`otoroshi-swagger-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-swagger-controller%22%29)\n* [`otoroshi-u2f-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-u2f-controller%22%29)\n* [`otoroshi-backoffice-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-backoffice-api%22%29)\n* [`otoroshi-health-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-api%22%29)\n* [`otoroshi-stats-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-stats-api%22%29)\n* [`otoroshi-admin-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-admin-api%22%29)\n* [`otoroshi-auth-modules-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-modules-api%22%29)\n* [`otoroshi-certificates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-api%22%29)\n* [`otoroshi-pki`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-pki%22%29)\n* [`otoroshi-scripts-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-scripts-api%22%29)\n* [`otoroshi-analytics-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-api%22%29)\n* [`otoroshi-import-export-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-import-export-api%22%29)\n* [`otoroshi-templates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-templates-api%22%29)\n* [`otoroshi-teams-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-teams-api%22%29)\n* [`otoroshi-events-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-api%22%29)\n* [`otoroshi-canary-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary-api%22%29)\n* [`otoroshi-data-exporter-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-api%22%29)\n* [`otoroshi-services-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-services-api%22%29)\n* [`otoroshi-tcp-service-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-service-api%22%29)\n* [`otoroshi-tenants-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tenants-api%22%29)\n* [`otoroshi-global-config-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config-api%22%29)\n* [`otoroshi-apikeys-fs-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fs-api%22%29)\n* [`otoroshi-apikeys-fg-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fg-api%22%29)\n* [`otoroshi-apikeys-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-api%22%29)\n* [`otoroshi-statsd-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-statsd-actor%22%29)\n* [`otoroshi-snow-monkey-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snow-monkey-api%22%29)\n* [`otoroshi-jobs-eventstore-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-eventstore-checker%22%29)\n* [`otoroshi-initials-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-initials-certs-job%22%29)\n* [`otoroshi-alert-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor%22%29)\n* [`otoroshi-alert-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor-supervizer%22%29)\n* [`otoroshi-alerts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alerts%22%29)\n* [`otoroshi-apikeys-secrets-rotation-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-secrets-rotation-job%22%29)\n* [`otoroshi-loader`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-loader%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-analytics-writes-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-writes-elastic%22%29)\n* [`otoroshi-analytics-reads-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-reads-elastic%22%29)\n* [`otoroshi-events-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-actor-supervizer%22%29)\n* [`otoroshi-data-exporter`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter%22%29)\n* [`otoroshi-data-exporter-update-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-update-job%22%29)\n* [`otoroshi-kafka-wrapper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-wrapper%22%29)\n* [`otoroshi-kafka-connector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-connector%22%29)\n* [`otoroshi-analytics-webhook`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-webhook%22%29)\n* [`otoroshi-jobs-software-updates`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-software-updates%22%29)\n* [`otoroshi-analytics-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor%22%29)\n* [`otoroshi-analytics-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor-supervizer%22%29)\n* [`otoroshi-analytics-event`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-event%22%29)\n* [`otoroshi-env`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-env%22%29)\n* [`otoroshi-script-compiler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-compiler%22%29)\n* [`otoroshi-script-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-manager%22%29)\n* [`otoroshi-script`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-custom-timeouts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-custom-timeouts%22%29)\n* [`otoroshi-client-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-config%22%29)\n* [`otoroshi-canary`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary%22%29)\n* [`otoroshi-redirection-settings`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redirection-settings%22%29)\n* [`otoroshi-service-descriptor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor%22%29)\n* [`otoroshi-service-descriptor-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor-datastore%22%29)\n* [`otoroshi-console-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-console-mailer%22%29)\n* [`otoroshi-mailgun-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailgun-mailer%22%29)\n* [`otoroshi-mailjet-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailjet-mailer%22%29)\n* [`otoroshi-sendgrid-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sendgrid-mailer%22%29)\n* [`otoroshi-generic-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-generic-mailer%22%29)\n* [`otoroshi-clevercloud-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-clevercloud-client%22%29)\n* [`otoroshi-metrics`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-metrics%22%29)\n* [`otoroshi-gzip-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-gzip-config%22%29)\n* [`otoroshi-regex-pool`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-regex-pool%22%29)\n* [`otoroshi-ws-client-chooser`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ws-client-chooser%22%29)\n* [`otoroshi-akka-ws-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-akka-ws-client%22%29)\n* [`otoroshi-http-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-implicits%22%29)\n* [`otoroshi-service-group`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-group%22%29)\n* [`otoroshi-data-exporter-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config%22%29)\n* [`otoroshi-data-exporter-config-migration-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config-migration-job%22%29)\n* [`otoroshi-lets-encrypt-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lets-encrypt-helper%22%29)\n* [`otoroshi-apkikey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apkikey%22%29)\n* [`otoroshi-error-template`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-template%22%29)\n* [`otoroshi-job-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-job-manager%22%29)\n* [`otoroshi-plugins-internal-eventlistener-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-internal-eventlistener-actor%22%29)\n* [`otoroshi-global-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config%22%29)\n* [`otoroshi-jwks`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwks%22%29)\n* [`otoroshi-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwt-verifier%22%29)\n* [`otoroshi-global-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-jwt-verifier%22%29)\n* [`otoroshi-snowmonkey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey-config%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-service-datatstore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-datatstore%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-redis-like-store`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-like-store%22%29)\n* [`otoroshi-globalconfig-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-globalconfig-datastore%22%29)\n* [`otoroshi-reactive-pg-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-datastores%22%29)\n* [`otoroshi-reactive-pg-kv`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-kv%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-apikey-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikey-datastore%22%29)\n* [`otoroshi-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastore%22%29)\n* [`otoroshi-certificate-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificate-datastore%22%29)\n* [`otoroshi-simple-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-simple-admin-datastore%22%29)\n* [`otoroshi-atomic-in-memory-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-atomic-in-memory-datastore%22%29)\n* [`otoroshi-lettuce-redis`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis%22%29)\n* [`otoroshi-lettuce-redis-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis-cluster%22%29)\n* [`otoroshi-redis-lettuce-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-lettuce-datastores%22%29)\n* [`otoroshi-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastores%22%29)\n* [`otoroshi-file-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-file-db-datastores%22%29)\n* [`otoroshi-http-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-db-datastores%22%29)\n* [`otoroshi-s3-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-s3-datastores%22%29)\n* [`PluginDocumentationGenerator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22PluginDocumentationGenerator%22%29)\n* [`otoroshi-health-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-checker%22%29)\n* [`otoroshi-healthcheck-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-job%22%29)\n* [`otoroshi-healthcheck-local-cache-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-local-cache-job%22%29)\n* [`otoroshi-plugins-response-cache`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-response-cache%22%29)\n* [`otoroshi-oidc-apikey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-oidc-apikey-config%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-info%22%29)\n* [`otoroshi-plugins-ipstack-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-ipstack-geolocation-info%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-helper%22%29)\n* [`otoroshi-plugins-user-agent-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-helper%22%29)\n* [`otoroshi-plugins-user-agent-extractor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-extractor%22%29)\n* [`otoroshi-global-el`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-el%22%29)\n* [`otoroshi-plugins-oauth1-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-oauth1-caller-plugin%22%29)\n* [`otoroshi-dynamic-sslcontext`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-dynamic-sslcontext%22%29)\n* [`otoroshi-plugins-access-log-clf`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-clf%22%29)\n* [`otoroshi-plugins-access-log-json`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-json%22%29)\n* [`otoroshi-plugins-kafka-access-log`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kafka-access-log%22%29)\n* [`otoroshi-plugins-kubernetes-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-client%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-sync%22%29)\n* [`otoroshi-plugins-kubernetes-crds-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-crds-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-sync%22%29)\n* [`otoroshi-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cluster%22%29)\n* [`otoroshi-crd-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-crd-validator%22%29)\n* [`otoroshi-sidecar-injector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sidecar-injector%22%29)\n* [`otoroshi-plugins-kubernetes-cert-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-cert-sync%22%29)\n* [`otoroshi-plugins-kubernetes-to-otoroshi-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-to-otoroshi-certs-job%22%29)\n* [`otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job%22%29)\n* [`otoroshi-apikeys-workflow-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-workflow-job%22%29)\n* [`otoroshi-cert-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-helper%22%29)\n* [`otoroshi-certificates-ocsp`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-ocsp%22%29)\n* [`otoroshi-claim`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-claim%22%29)\n* [`otoroshi-cert`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert%22%29)\n* [`otoroshi-ssl-provider`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-provider%22%29)\n* [`otoroshi-cert-data`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-data%22%29)\n* [`otoroshi-client-cert-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-cert-validator%22%29)\n* [`otoroshi-ssl-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-implicits%22%29)\n* [`otoroshi-saml-validator-utils`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-saml-validator-utils%22%29)\n* [`otoroshi-global-saml-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-saml-config%22%29)\n* [`otoroshi-plugins-hmac-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-caller-plugin%22%29)\n* [`otoroshi-plugins-hmac-access-validator-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-access-validator-plugin%22%29)\n* [`otoroshi-plugins-hasallowedusersvalidator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hasallowedusersvalidator%22%29)\n* [`otoroshi-auth-module-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-module-config%22%29)\n* [`otoroshi-basic-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-basic-auth-config%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n* [`otoroshi-plugins-jsonpath-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-jsonpath-helper%22%29)\n* [`otoroshi-global-oauth2-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-config%22%29)\n* [`otoroshi-global-oauth2-module`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-module%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n"},{"name":"end-to-end-mtls.md","id":"/how-to-s/end-to-end-mtls.md","url":"/how-to-s/end-to-end-mtls.html","title":"End-to-end mTLS","content":"# End-to-end mTLS\n\nIf you want to use MTLS on otoroshi, you first need to enable it. It is not enabled by default as it will make TLS handshake way heavier. \nTo enable it just change the following config :\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\nYou can use the `Want` setup if you cant to have both mtls on some services and no mtls on other services.\n\nYou can also change the trusted CA list sent in the handshake certificate request from the `Danger Zone` in `Tls Settings`.\n\nOtoroshi support mutual TLS out of the box. mTLS from client to Otoroshi and from Otoroshi to targets are supported. In this article we will see how to configure Otoroshi to use end-to-end mTLS. All code and files used in this articles can be found on the [Otoroshi github](https://github.com/MAIF/otoroshi/tree/master/demos/mtls)\n\n### Create certificates\n\nBut first we need to generate some certificates to make the demo work\n\n```sh\nmkdir mtls-demo\ncd mtls-demo\nmkdir ca\nmkdir server\nmkdir client\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-backend.key 4096\n# remove pass phrase\nopenssl rsa -in ./ca/ca-backend.key -out ./ca/ca-backend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-backend.key -out ./ca/ca-backend.cer -subj \"/CN=MTLSB\"\n\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-frontend.key 2048\n# remove pass phrase\nopenssl rsa -in ./ca/ca-frontend.key -out ./ca/ca-frontend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-frontend.key -out ./ca/ca-frontend.cer -subj \"/CN=MTLSF\"\n\n\n# now create the backend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.backend.oto.tools.key -out ./server/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.backend.oto.tools.key -sha256 -out ./server/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 1 -out ./server/_.backend.oto.tools.cer\n# verify the certificate, should output './server/_.backend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-backend.cer ./server/_.backend.oto.tools.cer\n\n\n# now create the frontend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.frontend.oto.tools.key -out ./server/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.frontend.oto.tools.key -sha256 -out ./server/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 1 -out ./server/_.frontend.oto.tools.cer\n# verify the certificate, should output './server/_.frontend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.cer\n\n\n# now create the client cert key for backend, use password as pass phrase\nopenssl genrsa -out ./client/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 2 -out ./client/_.backend.oto.tools.cer\n# generate a pem version of the cert and key, use password as password\nopenssl x509 -in client/_.backend.oto.tools.cer -out client/_.backend.oto.tools.pem -outform PEM\n\n\n# now create the client cert key for frontend, use password as pass phrase\nopenssl genrsa -out ./client/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 2 -out ./client/_.frontend.oto.tools.cer\n# generate a pkcs12 version of the cert and key, use password as password\n# openssl pkcs12 -export -clcerts -in client/_.frontend.oto.tools.cer -inkey client/_.frontend.oto.tools.key -out client/_.frontend.oto.tools.p12\nopenssl x509 -in client/_.frontend.oto.tools.cer -out client/_.frontend.oto.tools.pem -outform PEM\n```\n\nOnce it's done, you should have something like\n\n```sh\n$ tree\n.\n├── backend.js\n├── ca\n│   ├── ca-backend.cer\n│   ├── ca-backend.key\n│   ├── ca-frontend.cer\n│   └── ca-frontend.key\n├── client\n│   ├── _.backend.oto.tools.cer\n│   ├── _.backend.oto.tools.csr\n│   ├── _.backend.oto.tools.key\n│   ├── _.backend.oto.tools.pem\n│   ├── _.frontend.oto.tools.cer\n│   ├── _.frontend.oto.tools.csr\n│   ├── _.frontend.oto.tools.key\n│   └── _.frontend.oto.tools.pem\n└── server\n ├── _.backend.oto.tools.cer\n ├── _.backend.oto.tools.csr\n ├── _.backend.oto.tools.key\n ├── _.frontend.oto.tools.cer\n ├── _.frontend.oto.tools.csr\n └── _.frontend.oto.tools.key\n\n3 directories, 18 files\n```\n\n### The backend service \n\nnow, let's create a backend service using nodejs. Create a file named `backend.js`\n\n```sh\ntouch backend.js\n```\n\nand put the following content\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n}; \n\nconst server = https.createServer(options, (req, res) => { \n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nto run the server, just do \n\n```sh\nnode ./backend.js\n```\n\nnow you can try your server with\n\n```sh\ncurl --cacert ./ca/ca-backend.cer 'https://api.backend.oto.tools:8444/'\n```\n\nThis should output :\n```json\n{ \"message\": \"Hello World!\" }\n```\n\nnow modify your backend server to ensure that the client provides a client certificate like:\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n requestCert: true, \n rejectUnauthorized: true\n}; \n\nconst server = https.createServer(options, (req, res) => { \n console.log('Client certificate CN: ', req.socket.getPeerCertificate().subject.CN);\n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nyou can test your new server with\n\n```sh\ncurl \\\n --cacert ./ca/ca-backend.cer \\\n --cert ./client/_.backend.oto.tools.pem \\\n --key ./client/_.backend.oto.tools.key 'https://api.backend.oto.tools:8444/'\n```\n\nthe output should be :\n\n```json\n{ \"message\": \"Hello World!\" }\n```\n\n### Otoroshi setup\n\nDownload the latest version of the Otoroshi jar and run it like\n\n```sh\n java \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.ssl.fromOutside.clientAuth=Want \\\n -jar -Dotoroshi.storage=file otoroshi.jar\n\n[info] otoroshi-env - Admin API exposed on http://otoroshi-api.oto.tools:8080\n[info] otoroshi-env - Admin UI exposed on http://otoroshi.oto.tools:8080\n[info] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[info] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[info] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / password\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n[info] p.c.s.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:8443\n[info] otoroshi-env - Generating a self signed SSL certificate for https://*.oto.tools ...\n```\n\nand log into otoroshi with the tuple `admin@otoroshi.io / password` displayed in the logs. \n\nOnce logged in, navigate to the routes page and create a new route.\n\n* Set a name then validate the creation\n* On frontend node, add `api.frontend.oto.tools` in the list of domains\n* On backend node, replace the target with `api.backend.oto.tools` as hostname and `8444` as port. \n\nSave the route and try to call it.\n\n```sh\ncurl 'http://api.frontend.oto.tools:8080/'\n```\n\nThis should output :\n```json\n{\"Otoroshi-Error\": \"Something went wrong, you should try later. Thanks for your understanding.\"}\n```\n\nyou should get an error due to the fact that Otoroshi doesn't know about the server certificate and the client certificate expected by the server.\n\nWe must declare the client and server certificates for `https://api.backend.oto.tools` to Otoroshi. \n\nGo to the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) and create a new item. Drag and drop the content of the `./client/_.backend.oto.tools.cer` and `./client/_.backend.oto.tools.key` files, respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you prefer to use the API, you can create an Otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.backend.oto.tools.cer ./ca/ca-backend.cer ./server/_.backend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n --data-binary @- \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle \n```\n\nnow we have to expose `https://api.frontend.oto.tools:8443` using otoroshi. \n\nCreate a second item. Copy and paste the content of `./server/_.frontend.oto.tools.cer` and `./server/_.frontend.oto.tools.key` respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you don't want to bother with UI copy/paste, you can use the import bundle api endpoint to create an otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.frontend.oto.tools.cer ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n --data-binary @- \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle\n```\n\nOnce created, go back to your route. On the target of the backend node, we have to enable the custom Otoroshi TLS.\n\n* Click on the backend node\n* Click on your target\n* Click on the Show advanced settings button\n* Click on Custom TLS setup\n* Enable the section\n* In the list of certificates, select the backend certificate\n* In the list of trusted certificates, select the frontend certificate\n* Save your route\n \nTry the following command\n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\nNow we want to enforce the fact that we want client certificate for `api.frontend.oto.tools`. \n\nSearch in the list of plugins and add the `Client Certificate Only` plugin to your route.\n\nnow if you retry \n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nyou should get an error because no client certificate is passed with the request. But if you pass the `./client/_.frontend.oto.tools.csr` client cert and the key in your curl call\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n### Client certificate matching plugin\n\nOtoroshi can restrict and check all incoming client certificates on a route.\n\nSearch in the list of plugins the `Client certificate matching` plugin and add it the the flow.\n\nSave the route and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nOur client certificate is not matched by Otoroshi. We have to add the subject DN in the configuration of the `Client certificate matching` plugin to authorize it.\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [],\n \"subjectDNs\": [\n \"CN=*.frontend.oto.tools\"\n ],\n \"issuerDNs\": [],\n \"regexSubjectDNs\": [],\n \"regexIssuerDNs\": []\n }\n}\n```\n\nSave the service and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n\n"},{"name":"export-alerts-using-mailgun.md","id":"/how-to-s/export-alerts-using-mailgun.md","url":"/how-to-s/export-alerts-using-mailgun.html","title":"Send alerts using mailgun","content":"# Send alerts using mailgun\n\nAll Otoroshi alerts can be send on different channels.\nOne of the ways is to send a group of specific alerts via emails.\n\nTo enable this behaviour, let's start by create an exporter of events.\n\nIn this tutorial, we will admit that you already have a mailgun account with an API key and a domain.\n\n## Create an Mailgun exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `mailer` in the `type` selector field\n6. Jump to `Exporter config` and select the `Mailgun` option\n7. Set the following values:\n* `EU` : false/true depending on your mailgun configuratin\n* `Mailgun api key` : your-mailgun-api-key\n* `Mailgun domain` : your-mailgun-domain\n* `Email addresses` : list of the recipient adresses\n\nWith this configuration, all Otoroshi events will be send to your listed addresses (we don't recommended to do that).\n\nTo filter events on `Alerts` type, we need to add the following configuration inside the `Filtering and projection` section (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)).\n\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n``` \n\nSave at the bottom page and enable the exporter (on the top of the page or in list of exporters). We will need to wait few seconds to receive the first alerts.\n\nThe **projection** field can be useful in the case you want to filter the fields contained in each alert sent.\n\nThe `Projection` field is a json where you can list the fields to keep for each alert.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be sent to the addresses of your recipients."},{"name":"export-events-to-elastic.md","id":"/how-to-s/export-events-to-elastic.md","url":"/how-to-s/export-events-to-elastic.html","title":"Export events to Elasticsearch","content":"# Export events to Elasticsearch\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Deploy a Elasticsearch and kibana stack on Docker\n\nLet's start by creating an Elasticsearch and Kibana stack on our machine (if it's already done for you, you can skip this section).\n\nTo start an Elasticsearch container for development or testing, run:\n\n```sh\ndocker network create elastic\ndocker pull docker.elastic.co/elasticsearch/elasticsearch:7.15.1\ndocker run --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e \"discovery.type=single-node\" docker.elastic.co/elasticsearch/elasticsearch:7.15.1\n```\n\n```sh\ndocker pull docker.elastic.co/kibana/kibana:7.15.1\ndocker run --name kib01-test --net elastic -p 5601:5601 -e \"ELASTICSEARCH_HOSTS=http://es01-test:9200\" docker.elastic.co/kibana/kibana:7.15.1\n```\n\nTo access Kibana, go to @link:[http://localhost:5601](http://localhost:5601) { open=new }.\n\n### Create an Elasticsearch exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `elastic` in the `type` selector field\n6. Jump to `Exporter config`\n7. Set the following values: `Cluster URI` -> `http://localhost:9200`\n\nThen test your configuration by clicking on the `Check connection` button. This should output a modal with the Elasticsearch version and the number of loaded docs.\n\nSave at the bottom of the page and enable the exporter (on the top of the page or in list of exporters).\n\n### Testing your configuration\n\nOne simple way to test is to setup the reading of our Elasticsearch instance by Otoroshi.\n\nNavigate to the danger zone (click on the cog on the top right and scroll to `danger zone`). Jump to the `Analytics: Elastic dashboard datasource (read)` section.\n\nSet the following values : `Cluster URI` -> `http://localhost:9200`\n\nThen click on the `Check connection`. This should ouput the same result as the previous part. Save the global configuration and navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/stats](http://otoroshi.oto.tools:8080/bo/dashboard/stats) { open=new }.\n\nThis should output a list of graphs.\n\n### Advanced usage\n\nBy default, an exporter handle all events from Otoroshi. In some case, you need to filter the events to send to elasticsearch.\n\nTo filter the events, jump to the `Filtering and projection` field in exporter view. Otoroshi supports to include a kind of events or to exclude a list of events (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)). \n\nAn example which keep only events with a field `@type` of value `AlertEvent`:\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n```\nAn example which exclude only events with a field `@type` of value `GatewayEvent` :\n```json\n{\n \"exclude\": [\n { \"@type\": \"GatewayEvent\" }\n ],\n \"include\": []\n}\n```\n\nThe next field is the **Projection**. This field is a json when you can list the fields to keep for each event.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be send to ES.\n\n### Debug your configuration\n\n#### Missing user rights on Elasticsearch\n\nWhen creating an exporter, Otoroshi try to join the index route of the elasticsearch instance. If you have a specific management access rights on Elasticsearch, you have two possiblities :\n\n- set a full access to the user used in Otoroshi for write in Elasticsearch\n- set the version of Elasticsearch inside the `Version` field of your exporter.\n\n#### None event appear in your Elasticsearch\n\nWhen creating an exporter, Otoroshi try to push the index template on Elasticsearch. If the post failed, Otoroshi will fail for each push of events and your database will keep empty. \n\nTo fix this problem, you can try to send the index template with the `Manually apply index template` button in your exporter."},{"name":"import-export-otoroshi-datastore.md","id":"/how-to-s/import-export-otoroshi-datastore.md","url":"/how-to-s/import-export-otoroshi-datastore.html","title":"Import and export Otoroshi datastore","content":"# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`"},{"name":"index.md","id":"/how-to-s/index.md","url":"/how-to-s/index.html","title":"How to's","content":"# How to's\n\nin this section, we will explain some mainstream Otoroshi usage scenario's \n\n* @ref:[Otoroshi and WASM](./wasm-usage.md)\n* @ref:[Wasmo](./wasmo-installation.md)\n* @ref:[Tailscale integration](./tailscale-integration.md)\n* @ref:[End-to-end mTLS](./end-to-end-mtls.md)\n* @ref:[Send alerts by emails](./export-alerts-using-mailgun.md)\n* @ref:[Export events to Elasticsearch](./export-events-to-elastic.md)\n* @ref:[Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* @ref:[Secure an app with Auth0](./secure-app-with-auth0.md)\n* @ref:[Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* @ref:[Secure an app with LDAP](./secure-app-with-ldap.md)\n* @ref:[Secure an api with apikeys](./secure-with-apikey.md)\n* @ref:[Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* @ref:[Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* @ref:[Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* @ref:[TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* @ref:[Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* @ref:[Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* @ref:[TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* @ref:[The resources loader](./resources-loader.md)\n* @ref:[Log levels customization](./custom-log-levels.md)\n* @ref:[Initial state customization](./custom-initial-state.md)\n* @ref:[Communicate with Kafka](./communicate-with-kafka.md)\n* @ref:[Create your custom Authentication module](./create-custom-auth-module.md)\n* @ref:[Working with Eureka](./working-with-eureka.md)\n* @ref:[Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* @ref:[Quickly expose a website and static files](./zip-backend-plugin.md)\n\n@@@ index\n\n\n* [WASM usage](./wasm-usage.md)\n* [wasmo](./wasmo-installation.md)\n* [Tailscale integration](./tailscale-integration.md)\n* [End-to-end mTLS](./end-to-end-mtls.md)\n* [Send alerts by emails](./export-alerts-using-mailgun.md)\n* [Export events to Elasticsearch](./export-events-to-elastic.md)\n* [Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* [Secure an app with Auth0](./secure-app-with-auth0.md)\n* [Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* [Secure an app with LDAP](./secure-app-with-ldap.md)\n* [Secure an api with apikeys](./secure-with-apikey.md)\n* [Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* [Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* [Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* [TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* [Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* [Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* [TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* [The resources loader](./resources-loader.md)\n* [Log levels customization](./custom-log-levels.md)\n* [Initial state customization](./custom-initial-state.md)\n* [Communicate with Kafka](./communicate-with-kafka.md)\n* [Create your custom Authentication module](./create-custom-auth-module.md)\n* [Working with Eureka](./working-with-eureka.md)\n* [Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* [Zip Backend plugin](./zip-backend-plugin.md) \n@@@\n"},{"name":"instantiate-waf-coraza.md","id":"/how-to-s/instantiate-waf-coraza.md","url":"/how-to-s/instantiate-waf-coraza.html","title":"Instantiate a WAF with Coraza","content":"# Instantiate a WAF with Coraza\n\n
      \nRoute plugins:\nCoraza WAF\nOverride Host Header\n
      \n\nSometimes you may want to secure an app with a [Web Appplication Firewall (WAF)](https://en.wikipedia.org/wiki/Web_application_firewall) and apply the security rules from the [OWASP Core Rule Set](https://owasp.org/www-project-modsecurity-core-rule-set/). To allow that, we integrated [the Coraza WAF](https://coraza.io/) in Otoroshi through a plugin that uses the WASM version of Coraza.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a WAF configuration\n\nfirst, go on [the features page of otoroshi](http://otoroshi.oto.tools:8080/bo/dashboard/features) and then click on the [Coraza WAF configs. item](http://otoroshi.oto.tools:8080/bo/dashboard/extensions/coraza-waf/coraza-configs). \n\nNow create a new configuration, give it a name and a description, ensure that you enabled the `Inspect req/res body` flag and save your configuration.\n\nThe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Configure Coraza and the OWASP Core Rule Set\n\nNow you can easily configure the coraza WAF in the `json` config. section. By default it should look something like :\n\n```json\n{\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n}\n```\n\nYou can find anything about it in [the documentation of Coraza](https://coraza.io/docs/tutorials/introduction/).\n\nhere we have the basic setup to apply the OWASP core rule set in detection mode only. \nSo each time Coraza will find something weird in a request, it will only log it but let the request pass.\n We can enable blocking by setting `\"SecRuleEngine On\"`\n\nwe can also deny access to the `/admin` uri by adding the following directive\n\n```json\n\"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\"\n```\n\nYou can also provide multiple profile of rules in the `directives_map` with different names and use the `per_authority_directives` object to map hostnames to a specific profile.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs/coraza-waf-demo' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\",\n \"SecRuleEngine On\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Add the WAF plugin on your route\n\nNow you can create a new route that will use your WAF configuration. Let say we want a route on `http://wouf.oto.tools:8080` to goes to `https://www.otoroshi.io`. Now add the `Coraza WAF` plugin to your route and in the configuration select the configuration you created previously.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n -H 'Content-Type: application/json' -d '\n{\n \"id\": \"route_demo\",\n \"name\": \"WAF route\",\n \"description\": \"A new route with a WAF enabled\",\n \"frontend\": {\n \"domains\": [\n \"wouf.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"www.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.wasm.proxywasm.NgCorazaWAF\",\n \"config\": {\n \"ref\": \"coraza-waf-demo\"\n },\n \"plugin_index\": {\n \"validate_access\": 0,\n \"transform_request\": 0,\n \"transform_response\": 0\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"plugin_index\": {\n \"transform_request\": 1\n }\n }\n ]\n}'\n```\n\n### Try to use an exploit ;)\n\nlet try to trigger Coraza with a Log4Shell crafted request:\n\n```sh\ncurl 'http://wouf.oto.tools:9999' -H 'foo: ${jndi:rmi://foo/bar}' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nor access to `/admin`\n\n```sh\ncurl 'http://wouf.oto.tools:9999/admin' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nif you look at otoroshi logs you will find something like :\n\n```log\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell \n [file \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\"] [line \"10608\"] [id \"944150\"] [rev \"\"] \n [msg \"Potential Remote Command Execution: Log4j / Log4shell\"] [data \"\"] [severity \"critical\"] \n [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] [tag \"application-multi\"] \n [tag \"language-java\"] [tag \"platform-multi\"] [tag \"attack-rce\"] [tag \"OWASP_CRS\"] \n [tag \"capec/1000/152/137/6\"] [tag \"PCI/6.5.2\"] [tag \"paranoia-level/1\"] [hostname \"wwwwouf.oto.tools\"] \n [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Inbound Anomaly Score Exceeded (Total Score: 5) \n [file \"@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf\"] [line \"11029\"] [id \"949110\"] [rev \"\"] \n [msg \"Inbound Anomaly Score Exceeded (Total Score: 5)\"] \n [data \"\"] [severity \"emergency\"] [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] \n [tag \"anomaly-evaluation\"] [hostname \"wwwwouf.oto.tools\"] [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"uTYakrlgMBydVGLodbz\" context_id=3 action=\"deny\" phase=\"http_response_headers\"\n...\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. [file \"\"] [line \"12914\"] \n [id \"101\"] [rev \"\"] [msg \"\"] [data \"\"] [severity \"emergency\"] [ver \"\"] [maturity \"0\"] [accuracy \"0\"] \n [hostname \"wwwwouf.oto.tools\"] [uri \"/admin\"] [unique_id \"mqXZeMdzRaVAqIiqvHf\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"mqXZeMdzRaVAqIiqvHf\" context_id=2 action=\"deny\" phase=\"http_request_headers\"\n```\n\n### Generated events\n\neach time Coraza will generate log about vunerability detection, an event will be generated in otoroshi and exporter through the usual data exporter way. The event will look like :\n\n```json\n{\n \"@id\" : \"86b647450-3cc7-42a9-aaec-828d261a8c74\",\n \"@timestamp\" : 1684938211157,\n \"@type\" : \"CorazaTrailEvent\",\n \"@product\" : \"otoroshi\",\n \"@serviceId\" : \"--\",\n \"@service\" : \"--\",\n \"@env\" : \"prod\",\n \"level\" : \"ERROR\",\n \"msg\" : \"Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell\",\n \"fields\" : {\n \"hostname\" : \"wouf.oto.tools\",\n \"maturity\" : \"0\",\n \"line\" : \"10608\",\n \"unique_id\" : \"oNbisKlXWaCdXntaUpq\",\n \"tag\" : \"paranoia-level/1\",\n \"data\" : \"\",\n \"accuracy\" : \"0\",\n \"uri\" : \"/\",\n \"rev\" : \"\",\n \"id\" : \"944150\",\n \"client\" : \"127.0.0.1\",\n \"ver\" : \"OWASP_CRS/4.0.0-rc1\",\n \"file\" : \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\",\n \"msg\" : \"Potential Remote Command Execution: Log4j / Log4shell\",\n \"severity\" : \"critical\"\n },\n \"raw\" : \"[client \\\"127.0.0.1\\\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell [file \\\"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\\\"] [line \\\"10608\\\"] [id \\\"944150\\\"] [rev \\\"\\\"] [msg \\\"Potential Remote Command Execution: Log4j / Log4shell\\\"] [data \\\"\\\"] [severity \\\"critical\\\"] [ver \\\"OWASP_CRS/4.0.0-rc1\\\"] [maturity \\\"0\\\"] [accuracy \\\"0\\\"] [tag \\\"application-multi\\\"] [tag \\\"language-java\\\"] [tag \\\"platform-multi\\\"] [tag \\\"attack-rce\\\"] [tag \\\"OWASP_CRS\\\"] [tag \\\"capec/1000/152/137/6\\\"] [tag \\\"PCI/6.5.2\\\"] [tag \\\"paranoia-level/1\\\"] [hostname \\\"wouf.oto.tools\\\"] [uri \\\"/\\\"] [unique_id \\\"oNbisKlXWaCdXntaUpq\\\"]\\n\",\n}\n```"},{"name":"resources-loader.md","id":"/how-to-s/resources-loader.md","url":"/how-to-s/resources-loader.html","title":"The resources loader","content":"# The resources loader\n\nThe resources loader is a tool to create an Otoroshi resource from a raw content. This content can be found on each Otoroshi resources pages (services descriptors, apikeys, certificates, etc ...). To get the content of a resource as file, you can use the two export buttons, one to export as JSON format and the other as YAML format.\n\nOnce exported, the content of the resource can be import with the resource loader. You can import single or multiples resources on one time, as JSON and YAML format.\n\nThe resource loader is available on this route [`bo/dashboard/resources-loader`](http://otoroshi.oto.tools:8080/bo/dashboard/resources-loader).\n\nOn this page, you can paste the content of your resources and click on **Load resources**.\n\nFor each detected resource, the loader will display :\n\n* a resource name corresponding to the field `name` \n* a resource type corresponding to the type of created resource (ServiceDescriptor, ApiKey, Certificate, etc)\n* a toggle to choose if you want to include the element for the creation step\n* the updated status by the creation process\n\nOnce you have selected the resources to create, you can **Import selected resources**.\n\nOnce generated, all status will be updated. If all is working, the status will be equals to done.\n\nIf you want to get back to the initial page, you can use the **restart** button."},{"name":"secure-an-app-with-jwt-verifiers.md","id":"/how-to-s/secure-an-app-with-jwt-verifiers.md","url":"/how-to-s/secure-an-app-with-jwt-verifiers.html","title":"Secure an api with jwt verifiers","content":"# Secure an api with jwt verifiers\n\n
      \nRoute plugins:\nJwt verification only\n
      \n\nA Jwt verifier is the guard that verifies the signature of tokens in requests. \n\nA verifier can obvisouly verify or generate.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Your first jwt verifier\n\nLet's start by validating all incoming request tokens tokens on our simple route created in the @ref:[Before you start](#before-you-start) section.\n\n1. Navigate to the simple route\n2. Search in the list of plugins and add the `Jwt verification only` plugin on the flow\n3. Click on `Start by select or create a JWT Verifier`\n4. Create a new JWT verifier\n5. Set `simple-jwt-verifier` as `Name`\n6. Select `Hmac + SHA` as `Algo` (for this example, we expect tokens with a symetric signature), `512` as `SHA size` and `otoroshi` as `HMAC secret`\n7. Confirm the creation \n\nSave your route and try to call it\n\n```sh\ncurl -X GET 'http://myservice.oto.tools:8080/' --include\n```\n\nThis should output : \n```json\n{\n \"Otoroshi-Error\": \"error.expected.token.not.found\"\n}\n```\n\nA simple way to generate a token is to use @link:[jwt.io](http://jwt.io) { open=new }. Once navigate, define `HS512` as `alg` in header section and insert `otoroshi` as verify signature secret. \n\nOnce created, copy-paste the token from jwt.io to the Authorization header and call our service.\n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"X-JWT-Token: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `X-JWT-Token` in headers field. Its value is exactly the same as the passed token.\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"X-JWT-Token\": \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ipDFgkww51mSaSg_199BMRj4gK20LGz_czozu3u8rCFFO1X20MwcabSqEzUc0q4qQ4rjTxjoR4HeUDVcw8BxoQ\",\n ...\n }\n}\n```\n\n### Verify and generate a new token\n\nAn other feature is to verify the incomings tokens and generate new ones, with a different signature and claims. \n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify and re-sign JWT token`. \n2. Edit the name with `jwt-verify-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Save your verifier.\n\n> Note : the name of the verifier doesn't impact the identifier. So you can save the changes of your verifier without modifying the identifier used in your call. \n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"Authorization: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `authorization` in headers field. This time, the value are different and you can check his signature on @link:[jwt.io](https://jwt.io) { open=new } (the expected secret of the generated token is **otoroshi-internal-secret**)\n\n\n\n### Verify, transform and generate a new token\n\nThe most advanced verifier is able to do the same as the previous ones, with the ability to configure the token generation (claims, output header name).\n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify, transform and re-sign JWT token`. \n\n2. Edit the name with `jwt-verify-transform-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Set `Internal-Authorization` as `Header name`\n6. Set `key` on first field of `Rename token fields` and `from-otoroshi-verifier` on second field\n7. Set `generated-key` and `generated-value` as `Set token fields`\n8. Add `generated_at` and `${date}` as second field of `Set token fields` (Otoroshi supports an @ref:[expression language](../topics/expression-language.md))\n9. Save your verifier and try to call your service again.\n\nThis should output a json with `authorization` in headers field and our generate token in `Internal-Authorization`.\nOnce paste in @link:[jwt.io](https://jwt.io) { open=new }, you should have :\n\n\n\nYou can see, in the payload of your token, the two claims **from-otoroshi-verifier** and **generated-key** added during the generation of the token by the JWT verifier.\n"},{"name":"secure-app-with-auth0.md","id":"/how-to-s/secure-app-with-auth0.md","url":"/how-to-s/secure-app-with-auth0.html","title":"Secure an app with Auth0","content":"# Secure an app with Auth0\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Configure an Auth0 client\n\nThe first step of this tutorial is to setup an Auth0 application with the information of the instance of our Otoroshi.\n\nNavigate to @link:[https://manage.auth0.com](https://manage.auth0.com) { open=new } (create an account if it's not already done). \n\nLet's create an application when clicking on the **Applications** button on the sidebar. Then click on the **Create application** button on the top right.\n\n1. Choose `Regular Web Applications` as `Application type`\n2. Then set for example `otoroshi-client` as `Name`, and confirm the creation\n3. Jump to the `Settings` tab\n4. Scroll to the `Application URLs` section and add the following url as `Allowed Callback URLs` : `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n5. Set `https://otoroshi.oto.tools:8080/` as `Allowed Logout URLs`\n6. Set `https://otoroshi.oto.tools:8080` as `Allowed Web Origins` \n7. Save changes at the bottom of the page.\n\nOnce done, we have a full setup, with a client ID and secret at the top of the page, which authorizes our Otoroshi and redirects the user to the callback url when they log into Auth0.\n\n### Create an Auth0 provider module\n\nLet's back to Otoroshi to create an authentication module with `OAuth2 / OIDC provider` as `type`.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth provider` in the type selector field\n3. Then click on `Get from OIDC config` and paste `https://..auth0.com/.well-known/openid-configuration`. Replace the tenant name by the name of your tenant (displayed on the left top of auth0 page), and the region of the tenant (`eu` in my case).\n\nOnce done, set the `Client ID` and the `Client secret` from your Auth0 application. End the configuration with `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`.\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs).\n\n### Connect to Otoroshi with Auth0 authentication\n\nTo secure Otoroshi with your Auth0 configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n2. Scroll to the **BackOffice auth. settings**\n3. Select your last Authentication configuration (created in the previous section)\n4. Save the global configuration with the button on the top right\n\n#### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the *Login using third-party* button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the auth0 server login page\n4. Set your account credentials\n5. Good works! You're connected to Otoroshi with an Auth0 module.\n\n### Secure an app with Auth0 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new **Authentication module** configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a connected user by a third-party server is covered by another route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called **a private app** when it is protected by an Authentication module.\n\nWe can set the Authentication module on your route.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n5. Now you can try to call your route and see the Auth0 login page appears.\n\n\n"},{"name":"secure-app-with-keycloak.md","id":"/how-to-s/secure-app-with-keycloak.md","url":"/how-to-s/secure-app-with-keycloak.html","title":"Secure an app with Keycloak","content":"# Secure an app with Keycloak\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running a keycloak instance with docker\n\n```sh\ndocker run \\\n -p 8080:8080 \\\n -e KEYCLOAK_USER=admin \\\n -e KEYCLOAK_PASSWORD=admin \\\n --name keycloak-server \\\n --detach jboss/keycloak:15.0.1\n```\n\nThis should download the image of keycloak (if you haven't already it) and display the digest of the created container. This command mapped TCP port 8080 in the container to port 8080 of your laptop and created a server with `admin/admin` as admin credentials.\n\nOnce started, you can open a browser on @link:[http://localhost:8080](http://localhost:8080) { open=new } and click on `Administration Console`. Log to your instance with `admin/admin` as credentials.\n\nThe first step is to create a Keycloak client, an entity that can request Keycloak to authenticate a user. Click on the **clients** button on the sidebar, and then on **Create** button at the top right of the view.\n\nFill the client form with the following values.\n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Protocol`: `openid-connect`\n* `Root URL`: `http://otoroshi.oto.tools:8080/`\n\nValidate the creation of the client by clicking on the **Save** button.\n\nThe next step is to change the `Access Type` used by default. Jump to the `Access Type` field and select `confidential`. The confidential configuration force the client application to send at Keycloak a client ID and a client Secret. Scroll to the bottom of the page and save the configuration.\n\nNow scroll to the top of your page. Just at the right of the `Settings` tab, a new tab appeared : the `Credentials` page. Click on this tab, and make sure that `Client Id and Secret` is selected as `Client Authenticator` and copy the generated `Secret` to the next part.\n\n### Create a Keycloak provider module\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth2 / OIDC provider` in the type selector field\n3. Set a basic name and description\n\nA simple way to import a Keycloak client is to give the `URL of the OpenID Connect` Otoroshi. By default, keycloak used the next URL : `http://localhost:8080/auth/realms/master/.well-known/openid-configuration`. \n\nClick on the `Get from OIDC config` button and paste the previous link. Once it's done, scroll to the `URLs` section. All URLs has been fill with the values picked from the JSON object returns by the previous URL.\n\nThe only fields to change are : \n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Secret`: Paste the secret from the Credentials Keycloak page. In my case, it's something like `90c9bf0b-2c0c-4eb0-aa02-72195beb9da7`\n* `Callback URL`: `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs). Nothing else to change, just save the configuration.\n\n### Connect to Otoroshi with Keycloak authentication\n\nTo secure Otoroshi with your Keycloak configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new })\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the keycloak login page\n4. Set `admin/admin` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an Keycloak module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Visualize an admin user session or a private user session\n\nEach user, wheter connected user to the Otoroshi UI or at a private Otoroshi app, has an own session. As an administrator of Otoroshi, you can visualize via Otoroshi the list of the connected users and their profile.\n\nLet's start by navigating to the `Admin users sessions` page (just @link:[here](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/admin) or when clicking on the cog, and on the `Admins sessions` button at the bottom of the list).\n\nThis page gives a complete view of the connected admins. For each admin, you have his connection date and his expiration date. You can also check the `Profile` and the `Rights` of the connected users.\n\nIf we check the profile and the rights of the previously logged user (from Keycloak in the previous part) we can retrieve the following information :\n\n```json\n{\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"upn\": \"admin\",\n \"email_verified\": false,\n \"address\": {},\n \"groups\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ],\n \"preferred_username\": \"admin\"\n}\n```\n\nand his default rights \n\n```sh\n[\n {\n \"tenant\": \"default:rw\",\n \"teams\": [\n \"default:rw\"\n ]\n }\n]\n```\n\nWe haven't create any specific groups in Keycloak or specify rights in Otoroshi for him. In this case, the use received the default Otoroshi rights at his connection. The user can navigate on the default Organization and Teams (which are two resources created by Otoroshi at the boot) and have the full access on its (`r`: Read, `w`: Write, `*`: read/write).\n\nIn the same way, you'll find all users connected to a private Otoroshi app when navigate on the @link:[`Private App View`](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) or using the cog at the top of the page. \n\n### Configure the Keycloak module to force logged in users to be an Otoroshi admin with full access\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Supers admin only` button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, click on the cog button, and check that you have access to the full features of Otoroshi (like Admin user sessions). Now, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n]\n```\n\n### Merge Id token content on user profile\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Read profile` from token button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, your profile should be contains all Keycloak id token : \n```json\n{\n \"exp\": 1634286674,\n \"iat\": 1634286614,\n \"auth_time\": 1634286614,\n \"jti\": \"eb368578-e886-4caa-a51b-c1d04973c80e\",\n \"iss\": \"http://localhost:8080/auth/realms/master\",\n \"aud\": [\n \"master-realm\",\n \"account\"\n ],\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"typ\": \"Bearer\",\n \"azp\": \"keycloak-otoroshi-backoffice\",\n \"session_state\": \"e44fe471-aa3b-477d-b792-4f7b4caea220\",\n \"acr\": \"1\",\n \"allowed-origins\": [\n \"http://otoroshi.oto.tools:8080\"\n ],\n \"realm_access\": {\n \"roles\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ]\n },\n \"resource_access\": {\n \"master-realm\": {\n \"roles\": [\n \"view-identity-providers\",\n \"view-realm\",\n \"manage-identity-providers\",\n \"impersonation\",\n \"create-client\",\n \"manage-users\",\n \"query-realms\",\n \"view-authorization\",\n \"query-clients\",\n \"query-users\",\n \"manage-events\",\n \"manage-realm\",\n \"view-events\",\n \"view-users\",\n \"view-clients\",\n \"manage-authorization\",\n \"manage-clients\",\n \"query-groups\"\n ]\n },\n \"account\": {\n \"roles\": [\n \"manage-account\",\n \"manage-account-links\",\n \"view-profile\"\n ]\n }\n }\n ...\n}\n```\n\n### Manage the Otoroshi user rights from keycloak\n\nOne powerful feature supports by Otoroshi, is to use the Keycloak groups attributes to set a list of rights for a Otoroshi user.\n\nIn the Keycloak module, you have a field, named `Otoroshi rights field name` with `otoroshi_rights` as default value. This field is used by Otoroshi to retrieve information from the Id token groups.\n\nLet's create a group in Keycloak, and set our default Admin user inside.\nIn Keycloak admin console :\n\n1. Navigate to the groups view, using the keycloak sidebar\n2. Create a new group with `my-group` as `Name`\n3. Then, on the `Attributes` tab, create an attribute with `otoroshi_rights` as `Key` and the following json array as `Value`\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\nWith this configuration, the user have a full access on all Otoroshi resources (my-future-team is not created in Otoroshi but it's not a problem, Otoroshi can handle it and use this rights only when the team will be present)\n\nClick on the **Add** button and **save** the group. The last step is to assign our user to this group. Jump to `Users` view using the sidebar, click on **View all users**, edit the user and his group membership using the `Groups` tab (use **join** button the assign user in `my-group`).\n\nThe next step is to add a mapper in the Keycloak client. By default, Keycloak doesn't expose any users information (like group membership or users attribute). We need to ask to Keycloak to expose the user attribute `otoroshi_rights` set previously on group.\n\nNavigate to the `Keycloak-otoroshi-backoffice` client, and jump to `Mappers` tab. Create a new mapper with the following values: \n\n* Name: `otoroshi_rights`\n* Mapper Type: `User Attribute`\n* User Attribute: `otoroshi_rights`\n* Token Claim Name: `otoroshi_rights`\n* Claim JSON Type: `JSON`\n* Multivalued: `√`\n* Aggregate attribute values: `√`\n\nGo back to the Authentication Keycloak module inside Otoroshi UI, and turn off **Super admins only**. **Save** the configuration.\n\nOnce done, try again the connection to Otoroshi using Keycloak third-party server.\nNow, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\n### Secure an app with Keycloak authentication\n\nThe only change to apply on the previous authentication module is on the callback URL. When you want secure a Otoroshi service, and transform it on `Private App`, you need to set the `Callback URL` at `http://privateapps.oto.tools:8080/privateapps/generic/callback`. This configuration will redirect users to the backend service after they have successfully logged in.\n\n1. Go back to the authentication module\n2. Jump to the `Callback URL` field\n3. Paste this value `http://privateapps.oto.tools:8080/privateapps/generic/callback`\n4. Save your configuration\n5. Navigate to `http://myservice.oto.tools:8080`.\n6. You should redirect to the keycloak login page.\n7. Once logged in, you can check the content of the private app session created.\n\nThe rights should be : \n\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```"},{"name":"secure-app-with-ldap.md","id":"/how-to-s/secure-app-with-ldap.md","url":"/how-to-s/secure-app-with-ldap.html","title":"Secure an app and/or your Otoroshi UI with LDAP","content":"# Secure an app and/or your Otoroshi UI with LDAP\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n#### Running an simple OpenLDAP server \n\nRun OpenLDAP docker image : \n```sh\ndocker run \\\n -p 389:389 \\\n -p 636:636 \\\n --env LDAP_ORGANISATION=\"Otoroshi company\" \\\n --env LDAP_DOMAIN=\"otoroshi.tools\" \\\n --env LDAP_ADMIN_PASSWORD=\"otoroshi\" \\\n --env LDAP_READONLY_USER=\"false\" \\\n --env LDAP_TLS\"false\" \\\n --env LDAP_TLS_ENFORCE\"false\" \\\n --name my-openldap-container \\\n --detach osixia/openldap:1.5.0\n```\n\nLet's make the first search in our LDAP container :\n\n```sh\ndocker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=otoroshi,dc=tools -D \"cn=admin,dc=otoroshi,dc=tools\" -w otoroshi\n```\n\nThis should output :\n```sh\n# extended LDIF\n ...\n# otoroshi.tools\ndn: dc=otoroshi,dc=tools\nobjectClass: top\nobjectClass: dcObject\nobjectClass: organization\no: Otoroshi company\ndc: otoroshi\n\n# search result\nsearch: 2\nresult: 0 Success\n...\n```\n\nNow you can seed the open LDAP server with a few users. \n\nJoin your LDAP container.\n\n```sh\ndocker exec -it my-openldap-container \"/bin/bash\"\n```\n\nThe command `ldapadd` needs of a file to run.\n\nLaunch this command to create a `bootstrap.ldif` with one organization, one singers group with John user and a last group with Baz as scientist.\n\n```sh\necho -e \"\ndn: ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: People\n\ndn: ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: Role\n\ndn: uid=john,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: john\ncn: John\nsn: Brown\nmail: john@otoroshi.tools\npostalCode: 88442\nuserPassword: password\n\ndn: uid=baz,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: baz\ncn: Baz\nsn: Wilson\nmail: baz@otoroshi.tools\npostalCode: 88443\nuserPassword: password\n\ndn: cn=singers,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: singers\nmember: uid=john,ou=People,dc=otoroshi,dc=tools\n\ndn: cn=scientists,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: scientists\nmember: uid=baz,ou=People,dc=otoroshi,dc=tools\n\" > bootstrap.ldif\n\nldapadd -x -w otoroshi -D \"cn=admin,dc=otoroshi,dc=tools\" -f bootstrap.ldif -v\n```\n\n### Create an Authentication configuration\n\n- Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n- Click on the cog icon on the top right\n- Then `Authentication configs` button\n- And add a new configuration when clicking on the `Add item` button\n- Select the `Ldap auth. provider` in the type selector field\n- Set a basic name and description\n- Then set `ldap://localhost:389` as `LDAP Server URL`and `dc=otoroshi,dc=tools` as `Search Base`\n- Create a group filter (in the next part, we'll change this filter to spread users in different groups with given rights) with \n - objectClass=groupOfNames as `Group filter` \n - All as `Tenant`\n - All as `Team`\n - Read/Write as `Rights`\n- Set the search filter as `(uid=${username})`\n- Set `cn=admin,dc=otoroshi,dc=tools` as `Admin username`\n- Set `otoroshi` as `Admin password`\n- At the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n\n At this point, your configuration should be similar to :\n \n\n\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n- Test the connection when clicking on `Test admin connection` button. This should show a `It works!` message\n\n- Finally, test the user connection button and set `john/password` or `baz/password` as credentials. This should show a `It works!` message\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n\n### Connect to Otoroshi with LDAP authentication\n\nTo secure Otoroshi with your LDAP configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n- Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n- Scroll to the **BackOffice auth. settings**\n- Select your last Authentication configuration (created in the previous section)\n- Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n- Disconnect from your instance\n- Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080/backoffice/auth0/login](http://otoroshi.oto.tools:8080/backoffice/auth0/login) { open=new })\n- Set `john/password` or `baz/password` as credentials\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n\n#### Secure an app with LDAP authentication\n\nOnce the configuration is done, you can secure any of Otoroshi routes. \n\n- Navigate to any created route\n- Add the `Authentication` plugin to your route\n- Select your Authentication config inside the list\n- Save your configuration\n\nNow try to call your route. The login module should appear.\n\n#### Manage LDAP users rights on Otoroshi\n\nFor each group filter, you can affect a list of rights:\n\n- on an `Organization`\n- on a `Team`\n- and a level of rights : `Read`, `Write` or `Read/Write`\n\n\nStart by navigate to your authentication configuration (created in @ref:[previous](#create-an-authentication-configuration) step).\n\nThen, replace the values of the `Mapping group filter` field to match LDAP groups with Otoroshi rights.\n\n\n\n\nWith this configuration, Baz is an administrator of Otoroshi with full rights (read / write) on all organizations.\n\nConversely, John can't see any configuration pages (like the danger zone) because he has only the read rights on Otoroshi.\n\nYou can easily test this behaviour by @ref:[testing](#testing-your-configuration) with both credentials.\n\n\n#### Advanced usage of LDAP Authentication\n\nIn the previous section, we have define rights for each LDAP groups. But in some case, we want to have a finer granularity like set rights for a specific user. The last 4 fields of the authentication form cover this. \n\nLet's start by adding few properties for each connected users with `Extra metadata`.\n\n```json\n// Add this configuration in extra metadata part\n{\n \"provider\": \"OpenLDAP\"\n}\n```\n\nThe next field `Data override` is merged with extra metadata when a user connects to a `private app` or to the UI (inside Otoroshi, private app is a service secure by any authentication module). The `Email field name` is configured to match with the `mail` field from LDAP user data.\n\n```json \n{\n \"john@otoroshi.tools\": {\n \"stage_name\": \"Will\"\n }\n}\n```\n\nIf you try to connect to an app with this configuration, the user result profile should be :\n\n```json\n{\n ...,\n \"metadata\": {\n \"lastname\": \"Willy\",\n \"stage_name\": \"Will\"\n }\n}\n```\n\nLet's try to increase the John rights with the `Additional rights group`.\n\nThis field supports the creation of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n\n```json\n// increase_john_rights is a virtual group which adds full access rights at john \n{\n \"increase_john_rights\": {\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ],\n \"users\": [\n \"john@otoroshi.tools\"\n ]\n }\n}\n```\n\nThe last field `Rights override` is useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights. \n\nTo resume, when John connects to Otoroshi, he receives the rights to only read the default Organization (from **Mapping group filter**), then he is promote to administrator role (from **Additional rights group**) and finally his rights are reset with the last field **Rights override** to the read rights.\n\n```json \n{\n \"john@otoroshi.tools\": [\n {\n \"tenant\": \"*:r\",\n \"teams\": [\n \"*:r\"\n ]\n }\n ]\n}\n```\n\n\n\n\n\n\n\n\n"},{"name":"secure-the-communication-between-a-backend-app-and-otoroshi.md","id":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.md","url":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html","title":"Secure the communication between a backend app and Otoroshi","content":"# Secure the communication between a backend app and Otoroshi\n\n
      \nRoute plugins:\nOtoroshi challenge token\n
      \n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nLet's create a new route with the Otorochi challenge plugin enabled.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 8081,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiChallenge\",\n \"config\": {\n \"version\": 2,\n \"ttl\": 30,\n \"request_header_name\": \"Otoroshi-State\",\n \"response_header_name\": \"Otoroshi-State-Resp\",\n \"algo_to_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"algo_from_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"state_resp_leeway\": 10\n }\n }\n ]\n}\nEOF\n```\n\nLet's use the following application, developed in NodeJS, which supports both versions of the exchange protocol.\n\nClone this @link:[repository](https://github.com/MAIF/otoroshi/blob/master/demos/challenge) and run the installation of the dependencies.\n\n```sh\ngit clone 'git@github.com:MAIF/otoroshi.git' --depth=1\ncd ./otoroshi/demos/challenge\nnpm install\nPORT=8081 node server.js\n```\n\nThe last command should return : \n\n```sh\nchallenge-verifier listening on http://0.0.0.0:8081\n```\n\nThis project runs an express client with one middleware. The middleware handles each request, and check if the header `State token header` is present in headers. By default, the incoming expected header is `Otoroshi-State` by the application and `Otoroshi-State-Resp` header in the headers of the return request. \n\nTry to call your service via http://myapi.oto.tools:8080/. This should return a successful response with all headers received by the backend app. \n\nNow try to disable the middleware in the nodejs file by commenting the following line. \n\n```js\n// app.use(OtoroshiMiddleware());\n```\n\nTry to call again your service. This time, Otoroshi breaks the return response from your backend service, and returns.\n\n```sh\nDownstream microservice does not seems to be secured. Cancelling request !\n```"},{"name":"secure-with-apikey.md","id":"/how-to-s/secure-with-apikey.md","url":"/how-to-s/secure-with-apikey.html","title":"Secure an api with api keys","content":"# Secure an api with api keys\n\n
      \nRoute plugins:\nApikeys\n
      \n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n### Create a simple route\n\n**From UI**\n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/routes](http://otoroshi.oto.tools:8080/bo/dashboard/routes) { open=new } and click on the `create new route` button\n2. Give a name to your route\n3. Save your route\n4. Set `myservice.oto.tools` as frontend domains\n5. Set `https://mirror.otoroshi.io` as backend target (hostname: `mirror.otoroshi.io`, port: `443`, Tls: `Enabled`)\n\n**From Admin API**\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n### Secure routes with api key\n\nBy default, a route is public. In our case, we want to secure all paths starting with `/api` and leave all others unauthenticated.\n\nLet's add a new plugin, called `Apikeys`, to our route. Search in the list of plugins, then add it to the flow.\nOnce done, restrict its range by setting up `/api` in the `Informations>include` section.\n\n**From Admin API**\n\n```sh\ncurl -X PUT http://otoroshi-api.oto.tools:8080/api/routes/myservice \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/api\"\n ],\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to @link:[http://myservice.oto.tools:8080/api/test](http://myservice.oto.tools:8080/api/test) { open=new } again. If the service is configured, you should have a `Service Not found error`.\n\nThe expected error on the `/api/test`, indicate that an api key is required to access to this part of the backend service.\n\nNavigate to any other routes which are not starting by `/api/*` like @link:[http://myservice.oto.tools:8080/test/bar](http://myservice.oto.tools:8080/test/bar) { open=new }\n\n\n### Generate an api key to request secure services\n\nNavigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add](http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add) { open=new } or when clicking on the **Add apikey** button on the sidebar.\n\nThe only required fields of an Otoroshi api key are : \n\n* `ApiKey id`\n* `ApiKey Secret`\n* `ApiKey Name`\n\nThese fields are automatically generated by Otoroshi. However, you can override these values and indicate an additional description.\n\nTo simplify the rest of the tutorial, set the values:\n\n* `my-first-api-key-id` as `ApiKey Id`\n* `my-first-api-key-secret` as `ApiKey Secret`\n\nClick on **Create and stay on this ApiKey** button at the bottom of the page.\n\nNow you created the key, it's time to call our previous generated service with it.\n\nOtoroshi supports 4 methods to achieve that: \n\nFirst one by passing Otoroshi api key in two headers : `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` (these headers names can be override on each service).\nThe second by passing Otoroshi api key in the authentication Header (basically the `Authorization` header) as a basic encoded value. The third option is to use the bearer generated for your apikey (you can get it by calling `curl http://otoroshi-api.oto.tools:8080/api/apikeys/my-first-api-key-id/bearer`). A fourth option is to use jwt token but we will not review it here but you can find a @ref[tutorial here](./secure-with-oauth2-client-credentials.md).\n\nLet's go ahead and call our service with the first method :\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the second method using basic authentication:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nor\n\n```sh\ncurl -X GET \\\n -u my-first-api-key-id:my-first-api-key-secret \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the third method using otoroshi bearer:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Bearer otoapk_my-first-api-key-id_99cb8e081d692044593ad0e658a67a5114f7afbdcbeb26f8087cce0df3b610b2' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\n> Tips : To easily fill your headers, you can jump to the `Call examples` section in each api key view. In this section the header names are the default values and the service url is not set. You have to adapt these lines to your case. \n\n### Override defaults headers names for a route\n\nIn some case, we want to change the defaults headers names (and it's a quite good idea).\n\nLet's start by navigating to the `Apikeys` plugin in the Designer of our route.\n\nThe first values to change are the headers names used to read the api key from client. Start by clicking on `extractors > CustomHeaders` and set the following values :\n\n* `api-key-header-id` as `Custom client id header name`\n* `api-key-header-secret` as `Custom client secret header name`\n\nSave the route, and call the service again.\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output an error because Otoroshi are expecting the api keys in other headers.\n\n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nCall one again the service but with the changed headers names.\n\n```sh\ncurl -X GET \\\n -H 'api-key-header-id: my-first-api-key-id' \\\n -H 'api-key-header-secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nAll others default services will continue to accept the api keys with the `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` headers, whereas our service, will accept the `api-key-header-id` and `api-key-header-secret` headers.\n\n### Accept only api keys with expected values\n\nBy default, a secure service only accepts requests with api key. But all generated api keys are eligible to call our service and in some case, we want authorize only a couple of api keys.\n\nYou can restrict the list of accepted api keys by giving a list of `metadata` or/and `tags`. Each api key has a list of `tags` and `metadata`, which can be used by Otoroshi to validate a request with an api key. All api key metadata/tags can be forward to your service (see `Otoroshi Challenge` section of a service to get more information about `Otoroshi info. token`).\n\nLet's starting by only accepting api keys with the `otoroshi` tag.\n\nClick on the `ApiKeys` plugin, and enabled the `Routing` section. These constraints guarantee that a request will only be transmitted if all the constraints are validated.\n\nIn our first case, set `otoroshi` in `One Tag in` array and save the service.\nThen call our service with :\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output :\n```json\n// Error reason : Our api key doesn't contains the expected tag.\n{\n \"Otoroshi-Error\": \"Bad API key\"\n}\n```\n\nNavigate to the edit page of our api key, and jump to the `Metadata and tags` section.\nIn this section, add `otoroshi` in `Tags` array, then save the api key. Call once again your call and you will normally get a successful response of our backend service.\n\nIn this example, we have limited our service to API keys that have `otoroshi` as a tag.\n\nOtoroshi provides a few others behaviours. For each behaviour, *Api key used should*:\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n\n---\n\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n \n----\n\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata"},{"name":"secure-with-oauth1-client.md","id":"/how-to-s/secure-with-oauth1-client.md","url":"/how-to-s/secure-with-oauth1-client.html","title":"Secure an app with OAuth1 client flow","content":"# Secure an app with OAuth1 client flow\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running an simple OAuth 1 server\n\nIn this tutorial, we'll instantiate a oauth 1 server with docker. If you alredy have the necessary, skip this section @ref:[to](#create-an-oauth-1-provider-module).\n\nLet's start by running the server\n\n```sh\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://otoroshi.oto.tools:8080/backoffice/auth0/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nWe created a oauth 1 server which accepts `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Redirect URI`. This URL is used by Otoroshi to retrieve a token and a profile at the end of an authentication process.\n\nAfter this command, the container logs should output :\n```sh \n127.0.0.1 - - [14/Oct/2021 12:10:49] \"HEAD /api/health HTTP/1.1\" 200 -\n```\n\n### Create an OAuth 1 provider module\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then **Authentication configs** button\n1. And add a new configuration when clicking on the **Add item** button\n2. Select the `Oauth1 provider` in the type selector field\n3. Set a basic name and description like `oauth1-provider`\n4. Set `2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET` as `Consumer key`\n5. Set `wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp` as `Consumer secret`\n6. Set `http://localhost:5000/oauth/request_token` as `Request Token URL`\n7. Set `http://localhost:5000/oauth/authorize` as `Authorize URL`\n8. Set `http://localhost:oauth/access_token` as `Access token URL`\n9. Set `http://localhost:5000/api/me` as `Profile URL`\n10. Set `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`\n11. At the bottom of the page, disable the **secure** button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n At this point, your configuration should be similar to :\n\n\n\n\nWith this configuration, the connected user will receive default access on teams and organizations. If you want to change the access rights for a specific user, you can achieve it with the `Rights override` field and a configuration like :\n\n```json\n{\n \"foo@example.com\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n}\n```\n\nSave your configuration at the bottom of the page, then navigate to the `danger zone` to use your module as a third-party connection to the Otoroshi UI.\n\n### Connect to Otoroshi with OAuth1 authentication\n\nTo secure Otoroshi with your OAuth1 configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the oauth 1 server login page\n4. Set `example-user` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an OAuth1 module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Secure an app with OAuth 1 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new auth module configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a logged user by a third-party server is cover by an other route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called a private app when it is protected by an authentication module.\n\nOur example server supports only one redirect URI. We need to kill it, and to create a new container with `http://otoroshi.oto.tools:8080/privateapps/generic/callback` as `OAUTH1_REDIRECT_URI`\n\n```sh\ndocker rm -f oauth1-server\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://privateapps.oto.tools:8080/privateapps/generic/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nOnce the authentication module and the new container created, we can define the authentication module on the service.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n\nNow you can try to call your route and see the login module appears.\n\n> \n\nThe allow access to the user.\n\n> \n\nIf you had any errors, make sure of :\n\n* check if you are on http or https, and if the **secure cookie option** is enabled or not on the authentication module\n* check if your OAuth1 server has the REDIRECT_URI set on **privateapps/...**\n* Make sure your server supports POST or GET OAuth1 flow set on authentication module\n\nOnce the configuration is working, you can check, when connecting with an Otoroshi admin user, the `Private App session` created (use the cog at the top right of the page, and select `Priv. app sesssions`, or navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) { open=new }).\n\nOne interesing feature is to check the profile of the connected user. In our case, when clicking on the `Profile` button of the right user, we should have : \n\n```json\n{\n \"email\": \"foo@example.com\",\n \"id\": 1,\n \"name\": \"test name\",\n \"screen_name\": \"example-user\"\n}\n```"},{"name":"secure-with-oauth2-client-credentials.md","id":"/how-to-s/secure-with-oauth2-client-credentials.md","url":"/how-to-s/secure-with-oauth2-client-credentials.html","title":"Secure an app with OAuth2 client_credential flow","content":"# Secure an app with OAuth2 client_credential flow\n\n\n\nOtoroshi makes it easy for your app to implement the [OAuth2 Client Credentials Flow](https://auth0.com/docs/authorization/flows/client-credentials-flow). \n\nWith machine-to-machine (M2M) applications, the system authenticates and authorizes the app rather than a user. With the client credential flow, applications will pass along their Client ID and Client Secret to authenticate themselves and get a token.\n\n## Deployed the Client Credential Service\n\nThe Client Credential Service must be enabled as a global plugin on your Otoroshi instance. Once enabled, it will expose three endpoints to issue and validate tokens for your routes.\n\nLet's navigate to your otoroshi instance (in our case http://otoroshi.oto.tools:8080) on the danger zone (`top right cog icon / Danger zone` or at [/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone)).\n\nTo enable a plugin in global on Otoroshi, you must add it in the `Global Plugins` section.\n\n1. Open the `Global Plugin` section \n2. Click on `enabled` (if not already done)\n3. Search the plugin named `Client Credential Service` of type `Sink` (you need to enabled it on the old or new Otoroshi engine, depending on your use case)\n4. Inject the default configuration by clicking on the button (if you are using the old Otoroshi engine)\n\nIf you click on the arrow near each plugin, you will have the documentation of the plugin and its default configuration.\n\nThe client credential plugin has by default 4 parameters : \n\n* `domain`: a regex used to expose the three endpoints (`default`: *)\n* `expiration`: duration until the token expire (in ms) (`default`: 3600000)\n* `defaultKeyPair`: a key pair used to sign the jwt token. By default, Otoroshi is deployed with an otoroshi-jwt-signing that you can visualize on the jwt verifiers certificates (`default`: \"otoroshi-jwt-signing\")\n* `secure`: if enabled, Otoroshi will expose routes only in the https requests case (`default`: true)\n\nIn this tutorial, we will set the configuration as following : \n\n* `domain`: oauth.oto.tools\n* `expiration`: 3600000\n* `defaultKeyPair`: otoroshi-jwt-signing\n* `secure`: false\n\nNow that the plugin is running, third routes are exposed on each matching domain of the regex.\n\n* `GET /.well-known/otoroshi/oauth/jwks.json` : retrieve all public keys presents in Otoroshi\n* `POST /.well-known/otoroshi/oauth/token/introspect` : validate and decode the token \n* `POST /.well-known/otoroshi/oauth/token` : generate a token with the fields provided\n\nOnce the global configuration saved, we can deployed a simple service to test it.\n\nLet's navigate to the routes page, and create a new route with : \n\n1. `foo.oto.tools` as `domain` in the frontend node\n2. `mirror.otoroshi.io` as hostname in the list of targets of the backend node, and `443` as `port`.\n3. Search in the list of plugins and add the `Apikeys` plugin to the flow\n4. In the extractors section of the `Apikeys` plugin, disabled the `Basic`, `Client id` and `Custom headers` option.\n5. Save your route\n\nLet's make a first call, to check if the jwks are already exposed :\n\n```sh\ncurl 'http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json'\n```\n\nThe output should look like a list of public keys : \n```sh\n{\n \"keys\": [\n {\n \"kty\": \"RSA\",\n \"e\": \"AQAB\",\n \"kid\": \"otoroshi-intermediate-ca\",\n ...\n }\n ...\n ]\n}\n``` \n\nLet's make a call to your route. \n\n```sh\ncurl 'http://foo.oto.tools:8080/'\n```\n\nThis should output the expected error: \n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nThe first step is to generate an api key. Navigate to the api keys page, and create an item with the following values (it will be more easy to use them in the next step)\n\n* `my-id` as `ApiKey Id`\n* `my-secret` as `ApiKey Secret`\n\nThe next step is to get a token by calling the endpoint `http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json`. The required fields are the grand type, the client and the client secret corresponding to our generated api key.\n\n```sh\ncurl -X POST http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/token \\\n-H \"Content-Type: application/json\" \\\n-d @- <<'EOF'\n{\n \"grant_type\": \"client_credentials\",\n \"client_id\":\"my-id\",\n \"client_secret\":\"my-secret\"\n}\nEOF\n```\n\nThis request have one more optional field, named `scope`. The scope can be used to set a bunch of scope on the generated access token.\n\nThe last command should look like : \n\n```sh\n{\n \"access_token\": \"generated-token-xxxxx\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600\n}\n```\n\nNow we can call our api with the generated token\n\n```sh\ncurl 'http://foo.oto.tools:8080/' \\\n -H \"Authorization: Bearer generated-token-xxxxx\"\n```\n\nThis should output a successful call with the list of headers with a field named `Authorization` containing the previous access token.\n\n## Other possible configuration\n\nBy default, Otoroshi generate the access token with the specified key pair in the configuration. But, in some case, you want a specific key pair by client_id/client_secret.\nThe `jwt-sign-keypair` metadata can be set on any api key with the id of the key pair as value. \n"},{"name":"setup-otoroshi-cluster.md","id":"/how-to-s/setup-otoroshi-cluster.md","url":"/how-to-s/setup-otoroshi-cluster.html","title":"Setup an Otoroshi cluster","content":"# Setup an Otoroshi cluster\n\n
      \nRoute plugins:\nAdditional Headers In\n
      \n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n"},{"name":"tailscale-integration.md","id":"/how-to-s/tailscale-integration.md","url":"/how-to-s/tailscale-integration.html","title":"Tailscale integration","content":"# Tailscale integration\n\n[Tailscale](https://tailscale.com/) is a VPN service that let you create your own private network based on [Wireguard](https://www.wireguard.com/). Tailscale goes beyond the simple meshed wireguard based VPN and offers out of the box NAT traversal, third party identity provider integration, access control, magic DNS and let's encrypt integration for the machines on your VPN.\n\nOtoroshi provides somes plugins out of the box to work in a [Tailscale](https://tailscale.com/) environment.\n\nby default Otoroshi, works out of the box when integrated in a `tailnet` as you can contact other machines usign their ip address. But we can go a little bit further.\n\n## tailnet configuration\n\nfirst thing, go to your tailnet setting on [tailscale.com](https://login.tailscale.com/admin/machines) and go to the [DNS tab](https://login.tailscale.com/admin/dns). Here you can find \n\n* your tailnet name: the domain name of all your machines on your tailnet\n* MagicDNS: a way to address your machines by directly using their names\n* HTTPS Certificates: HTTPS certificates provision for all your machines\n\nto use otoroshi Tailscale plugin you must enable `MagicDNS` and `HTTPS Certificates`\n\n## Tailscale certificates integration\n\nyou can use tailscale generated let's encrypt certificates in otoroshi by using the `Tailscale certificate fetcher job` in the plugins section of the danger zone. Once enabled, this job will fetch certificates for domains in `xxxx.ts.net` that belong to your tailnet. \n\nas usual, the fetched certificates will be available in the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) of otoroshi.\n\n## Tailscale targets integration\n\nthe following pair of plugins let your contact tailscale machine by using their names even if their are multiple instance.\n\nwhen you register a machine on a tailnet, you have to provide a name for it, let say `my-server`. This machine will be addressable in your tailnet with `my-server.tailxxx.ts.net`. But if you have multiple instance of the same server on several machines with the same `my-server` name, their DNS name on the tailnet will be `my-server.tailxxx.ts.net`, `my-server-1.tailxxx.ts.net`, `my-server-2.tailxxx.ts.net`, etc. If you want to use those names in an otoroshi backend it could be tricky if the application has something like autoscaling enabled.\n\nin that case, you can add the `Tailscale targets job` in the plugins section of the danger zone. Once enabled, this job will fetch periodically available machine on the tailnet with their names and DNS names. Then, in a route, you can use the `Tailscale select target by name` plugin to tell otoroshi to loadbalance traffic between all machine that have the name specified in the plugin config. instead of their DNS name."},{"name":"tls-termination-using-own-certificates.md","id":"/how-to-s/tls-termination-using-own-certificates.md","url":"/how-to-s/tls-termination-using-own-certificates.html","title":"TLS termination using your own certificates","content":"# TLS termination using your own certificates\n\nThe goal of this tutorial is to expose a service via https using a certificate generated by openssl.\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nTry to call the service.\n\n```sh\ncurl 'http://myservice.oto.tools:8080'\n```\n\nThis should output something like\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463145856319359618\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8080\",\n \"opun-gateway-request-id\": \"1463145856554240100\",\n \"x-forwarded-proto\": \"https\",\n },\n \"body\": \"\"\n}\n```\n\nLet's try to call the service in https.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nThis should output\n\n```sh\ncurl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to myservice.oto.tools:8443\n```\n\nTo fix it, we have to generate a certificate and import it in Otoroshi to match the domain `myservice.oto.tools`.\n\n> If you already had a certificate you can skip the next set of commands and directly import your certificate in Otoroshi\n\nWe will use openssl to generate a private key and a self-signed certificate.\n\n```sh\nopenssl genrsa -out myservice.key 4096\n# remove pass phrase\nopenssl rsa -in myservice.key -out myservice.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key myservice.key -out myservice.cer -subj \"/CN=myservice.oto.tools\"\n```\n\nCheck the content of the certificate \n\n```sh\nopenssl x509 -in myservice.cer -text\n```\n\nThis should contains something like\n\n```sh\nCertificate:\n Data:\n Version: 1 (0x0)\n Serial Number: 9572962808320067790 (0x84d9fef455f188ce)\n Signature Algorithm: sha256WithRSAEncryption\n Issuer: CN=myservice.oto.tools\n Validity\n Not Before: Nov 23 14:25:55 2021 GMT\n Not After : Nov 23 14:25:55 2022 GMT\n Subject: CN=myservice.oto.tools\n Subject Public Key Info:\n Public Key Algorithm: rsaEncryption\n Public-Key: (4096 bit)\n Modulus:\n...\n```\n\nOnce generated, go back to Otoroshi and navigate to the certificates management page (`top right cog icon / SSL/TLS certificates` or at @link:[`/bo/dashboard/certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates)) and click on `Add item`.\n\nSet `myservice-certificate` as `name` and `description`.\n\nDrop the `myservice.cer` file or copy the content to the `Certificate full chain` field.\n\nDo the same action for the `myservice.key` file in the `Certificate private key` field.\n\nSet your passphrase password in the `private key password` field if you added one.\n\nLet's try the same call to the service.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nAn error should occurs due to the untrsuted received certificate server\n\n```sh\ncurl: (60) SSL certificate problem: self signed certificate\nMore details here: https://curl.haxx.se/docs/sslcerts.html\n\ncurl failed to verify the legitimacy of the server and therefore could not\nestablish a secure connection to it. To learn more about this situation and\nhow to fix it, please visit the web page mentioned above.\n```\n\nEnd this tutorial by trusting the certificate server \n\n```sh\ncurl 'https://myservice.oto.tools:8443' --cacert myservice.cer\n```\n\nThis should finally output\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463158439730479893\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8443\",\n \"opun-gateway-request-id\": \"1463158439558515871\",\n \"x-forwarded-proto\": \"https\",\n \"sozu-id\": \"01FN6MGKSYZNJYHEMP4R5PJ4Q5\"\n },\n \"body\": \"\"\n}\n```\n\n"},{"name":"tls-using-lets-encrypt.md","id":"/how-to-s/tls-using-lets-encrypt.md","url":"/how-to-s/tls-using-lets-encrypt.html","title":"TLS termination using Let's Encrypt","content":"# TLS termination using Let's Encrypt\n\nAs you know, Otoroshi is capable of doing TLS termination for your services. You can import your own certificates, generate certificates from scratch and you can also use the @link:[ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555) to generate certificates. One of the most popular service offering ACME certificates creation is @link:[Let's Encrypt](https://letsencrypt.org/).\n\n@@@ warning\nIn order to make this tutorial work, your otoroshi instance MUST be accessible from the internet in order to be reachable by Let's Encrypt ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n@@@ note\nthis tutorial can work with any ACME provider with the same rules. your otoroshi instance MUST be accessible by the ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n## Setup let's encrypt on otoroshi\n\nGo on the danger zone page by clicking on the [`cog icon / Danger Zone`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Scroll to the `Let's Encrypt settings` section. Enable it, and specify the address of the ACME server (for production Let's Encrypt it's `acme://letsencrypt.org`, for testing, it's `acme://letsencrypt.org/staging`. Any ACME server address should work). You can also add one or more email addresses or contact urls that will be included in your Let's Encrypt account. You don't have to fill the `public/private key` inputs as they will be automatically generated on the first usage.\n\n## Creating let's encrypt certificate from FQDNs\n\nYou can go to the certificates page by clicking on the [`cog icon / SSL/TLS Certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Here, click on the `+ Let's Encrypt certificate` button. A popup will show up to ask you the FQDN that you want for you certificate. Once done, click on the `Create` button. A few moment later, you will be redirected on a brand new certificate generated by Let's encrypt. You can now enjoy accessing your service behind the FQDN with TLS.\n\n## Creating let's encrypt certificate from a service\n\nYou can go to any service page and enable the flag `Issue Let's Encrypt cert.`. Do not forget to save your service. A few moment later, the certificates will be available in the certificates page and you can will be able to enjoy accessing your service with TLS.\n"},{"name":"wasm-usage.md","id":"/how-to-s/wasm-usage.md","url":"/how-to-s/wasm-usage.html","title":"Using wasm plugins","content":"# Using wasm plugins\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. You can find more about those plugins @ref:[here](../topics/wasm-usage.md)\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create the route with the plugin validator](#create-the-route-with-the-plugin-validator)\n3. [Test your validator](#test-your-validator)\n4. [Update the route by replacing the backend with a WASM file](#update-the-route-by-replacing-the-backend-with-a-wasm-file)\n5. [WASM backend test](#wasm-backend-test)\n6. [Expose a single file as WASM backend](#expose-a-single-file-as-wasm-backend)\n\nAfter completing these steps you will have a route that uses WASM plugins written in Rust.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create the route with the plugin validator\n\nFor this tutorial, we will start with an existing wasm file. The main function of this file will check the value of an http header to allow access or not. The can find this file at [https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm)\n\nThe main function of this validator, written in rust, should look like:\n\nvalidator.rs\n: @@snip [validator.rs](../snippets/wasmo/validator.rs) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.ts\n: @@snip [validator.ts](../snippets/wasmo/validator.ts) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.go\n: @@snip [validator.js](../snippets/wasmo/validator.go) \n\nThe plugin receives the request context from Otoroshi (the matching route, the api key if present, the headers, etc) as `WasmAccessValidatorContext` object. \nThen it applies a check on the headers, and responds with an error or success depending on the content of the foo header. \nObviously, the previous snippet is an example and the editor allows you to write whatever you want as a check.\n\nLet's create a route that uses the previous wasm file as an access validator plugin :\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThis request will apply the following process:\n\n* names the route *demo-otoroshi*\n* creates a frontend exposed on the `demo-otoroshi.oto.tools` \n* forward requests on one target, reachable at `mirror.otoroshi.io` using TLS on port 443\n* adds the *WasmAccessValidator* plugin to validate access based on the foo header to the route\n\nYou can validate the route creation by navigating to the [dashboard](http://otoroshi.oto.tools:8080/bo/dashboard/routes/demo-otoroshi?tab=flow)\n\n## Test your validator\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -I\n```\n\nThis should output the following error:\n\n```\nHTTP/1.1 401 Unauthorized\n```\n\nLet's call again the route by adding the header foo with the bar value.\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -I\n```\n\nThis should output the successfull message:\n\n```\nHTTP/1.1 200 OK\n```\n\n## Update the route by replacing the backend with a WASM file\n\nThe next step in this tutorial is to use a WASM file as backend of the route. We will use an existing WASM file, available in our wasm demos repository on github. \nThe content of this plugin, called `wasm-target.wasm`, looks like:\n\ntarget.rs\n: @@snip [target.rs](../snippets/wasmo/target.rs) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.ts\n: @@snip [target.ts](../snippets/wasmo/target.ts) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.go\n: @@snip [target.js](../snippets/wasmo/target.go) \n\nLet's explain this snippet. The purpose of this type of plugin is to respond an HTTP response with http status, body and headers map.\n\n1. Includes all public structures from `types.rs` file. This file contains predefined Otoroshi structures that plugins can manipulate.\n2. Necessary imports. [Extism](https://extism.org/docs/overview)'s goal is to make all software programmable by providing a plug-in system. \n3. Creates a map of new headers that will be merged with incoming request headers.\n4. Creates the response object with the map of merged headers, a simple JSON body and a successfull status code.\n\nThe file is downloadable [here](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm).\n\nLet's update the route using the this wasm file.\n\n```sh\ncurl -X PUT \"http://otoroshi-api.oto.tools:8080/api/routes/demo-otoroshi\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmBackend\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThe response should contains the updated route content.\n\n## WASM backend test\n\nLet's call our route.\n\n```sh\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -H \"fifi: foo\" -v\n```\n\nThis should output:\n\n```\n* Trying 127.0.0.1:8080...\n* Connected to demo-otoroshi.oto.tools (127.0.0.1) port 8080 (#0)\n> GET / HTTP/1.1\n> Host: demo-otoroshi.oto.tools:8080\n> User-Agent: curl/7.79.1\n> Accept: */*\n> foo:bar\n> fifi:foo\n>\n* Mark bundle as not supporting multiuse\n< HTTP/1.1 200 OK\n< foo: bar\n< Host: demo-otoroshi.oto.tools:8080\n<\n* Closing connection 0\n{\"foo\": \"bar\"}\n```\n\nIn this response, we can find our headers send in the curl command and those added by the wasm plugin.\n\n\n## Expose a single file as WASM backend\n\nA WASM backend plugin can directly expose a file written in Wasmo. This is the simplest possibility to write HTML, Javascript, JSON or expose a simple PNG image.\n\nLet's expose a HTML page. In your Wasmo instance, execute the following instructions:\n\n1. Click the new plugin button\n2. Add a name and `validate`\n3. Click the new plugin\n4. Create a new file named `index.html`\n5. Copy and paste the following content\n\n```html\n \n\n\n Wasmo plugin\n\n\n

      Hello from Wasmo

      \n\n\n```\n\nThis snippet is a short HTML template with a title to indicate that it comes from Wasmo. \n\nNow we can write our javascript function to parse and return the content of our HTML to Otoroshi. \n\n1. Navigate to the `index.js` file\n2. Replace the content with the following content\n\n```js\nimport IndexPage from './index.html'\n\nexport function execute() {\n \n let response = {\n headers: {\n 'Content-Type': 'text/html; charset=utf-8'\n },\n body: IndexPage,\n status: 200\n };\n \n Host.outputString(JSON.stringify(response));\n\n return 0;\n}\n```\n\nThe code is pretty self-explanatory. We start by importing our HTML page and we build the response with the correct content type, the body and a 200 http status.\n\nJust before testing, we need to change the esbuild configuration to specify how to bundle the HTML file.\n\nThe contents of your `esbuild.js` file should look this:\n\n```js\nconst esbuild = require('esbuild');\n\nesbuild\n .build({\n entryPoints: ['index.js'],\n outdir: 'dist',\n bundle: true,\n loader: {\n '.html': 'text'\n },\n sourcemap: true,\n minify: false, // might want to use true for production build\n format: 'cjs', // needs to be CJS for now\n target: ['es2020'] // don't go over es2020 because quickjs doesn't support it\n })\n```\n\nCheck your browser at `http://demo-otoroshi.oto.tools:8080` and you should see your page content updated to the new text.\n\nIf you need to expose more than a HTML page, we highly recommend to use the @ref:[Zip Backend plugin](../how-to-s/zip-backend-plugin.md)"},{"name":"wasmo-installation.md","id":"/how-to-s/wasmo-installation.md","url":"/how-to-s/wasmo-installation.html","title":"Deploy your own Wasmo","content":"# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: admin-api-apikey-id\n* `Apikey id`: admin-api-apikey-secret\n* `Apikey secret`: http://localhost:5001\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n"},{"name":"working-with-eureka.md","id":"/how-to-s/working-with-eureka.md","url":"/how-to-s/working-with-eureka.html","title":"Working with Eureka","content":"# Working with Eureka\n\n\n\nEureka is a library of Spring Cloud Netflix, which provides two parts to register and discover services.\nGenerally, the services are applications written with Spring but Eureka also provides a way to communicate in REST. The main goals of Eureka are to allow clients to find and communicate with each other without hard-coding the hostname and port.\nAll services are registered in an Eureka Server.\n\nTo work with Eureka, Otoroshi has three differents plugins:\n\n* to expose its own Eureka Server instance\n* to discover an existing Eureka Server instance\n* to use Eureka application as an Otoroshi target and took advantage of all Otoroshi clients features (load-balancing, rate limiting, etc...)\n\nLet's cut this tutorial in three parts. \n\n- Create an simple Spring application that we'll use as an Eureka Client\n- Deploy an implementation of the Otoroshi Eureka Server (using the `Eureka Instance` plugin), register eureka clients and expose them using the `Internal Eureka Server` plugin\n- Deploy an Netflix Eureka Server and use it in Otoroshi to discover apps using the `External Eureka Server` plugin.\n\n\nIn this tutorial: \n\n- [Create an Otoroshi route with the Internal Eureka Server plugin](#create-an-otoroshi-route-with-the-internal-eureka-server-plugin)\n- [Create a simple Eureka Client and register it](#create-a-simple-eureka-client-and-register-it)\n- [Connect to an external Eureka server](#connect-to-an-external-eureka-server)\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create an Otoroshi route with the Internal Eureka Server plugin\n\n@@@ note\nWe'll supposed that you have an Otoroshi exposed on the 8080 port with the new Otoroshi engine enabled\n@@@\n\nLet's jump to the routes Otoroshi [view](http://otoroshi.oto.tools:8080/bo/dashboard/routes) and create a new route using the wizard button.\n\nEnter the following values in for each step:\n\n1. An Eureka Server instance\n2. Choose the first choice : **BLANK ROUTE** and click on continue\n3. As exposed domain, set `eureka-server.oto.tools/eureka`\n4. As Target URL, set `http://foo.bar` (this value has no importance and will be skip by the Otoroshi Instance plugin)\n5. Validate the creation\n\nOnce created, you can hide with the arrow on the right top of the screen the tester view (which is displayed by default after each route creation).\nIn our case, we want to add a new plugin, called Internal Eureka Instance on our feed.\n\nInside the designer view:\n\n1. Search the `Eureka Instance` in the list of plugins.\n2. Add it to the feed by clicking on it\n3. Set an eviction timeout at 300 seconds (this configuration is used by Otoroshi to automatically check if an Eureka is up. Otherwise Otoroshi will evict the eureka client from the registry)\n\nWell done you have set up an Eureka Server. To check the content of an Eureka Server, you can navigate to this [link]('http://otoroshi.oto.tools:8080/bo/dashboard/eureka-servers'). In all case, none instances or applications are registered, so the registry is currently empty.\n\n### Create a simple Eureka Client and register it\n\n*This tutorial has no vocation to teach you how to write an Spring application and it may exists a newer version of this Spring code.*\n\n\nFor this tutorial, we'll use the following code which initiates an Eureka Client and defines an Spring REST Controller with only one endpoint. This endpoint will return its own exposed port (this value will be useful to check that the Otoroshi load balancing is right working between the multiples Eureka instances registered).\n\n\nLet's fast create a Spring project using [Spring Initializer](https://start.spring.io/). You can use the previous link or directly click on the following link to get the form already filled with the needed dependencies.\n\n````bash\nhttps://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.3&packaging=jar&jvmVersion=17&groupId=otoroshi.io&artifactId=eureka-client&name=eureka-client&description=A%20simple%20eureka%20client&packageName=otoroshi.io.eureka-client&dependencies=cloud-eureka,web\n````\n\nFeel free to change the project metadata for your use case.\n\nOnce downloaded and uncompressed, let's ahead and start to delete the application.properties and create an application.yml (if you are more comfortable with an application.properties, keep it)\n\n````yaml\neureka:\n client:\n fetch-registry: false # disable the discovery services mechanism for the client\n serviceUrl:\n defaultZone: http://eureka-server.oto.tools:8080/eureka\n\nspring:\n application:\n name: foo_app\n\n````\n\n\nNow, let's define the simple REST controller to expose the client port.\n\nCreate a new file, called PortController.java, in the sources folder of your project with the following content.\n\n````java\npackage otoroshi.io.eurekaclient;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.env.Environment;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class PortController {\n\n @Autowired\n Environment environment;\n\n @GetMapping(\"/port\")\n public String index() {\n return environment.getProperty(\"local.server.port\");\n }\n}\n````\nThis controller is very simple, we just exposed one endpoint `/port` which returns the port as string. Our client is ready to running. \n\nLet's launch it with the following command:\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085\n````\n\n@@@note\nThe port is not required but it will be useful when we will deploy more than one instances in the rest of the tutorial\n@@@\n\n\nOnce the command ran, you can navigate to the eureka server view in the Otoroshi UI. The dashboard should displays one registered app and instance.\nIt should also displays a timer for each application which represents the elapsed time since the last received heartbeat.\n\nLet's define a new route to exposed our registered eureka client.\n\n* Create a new route, named `Eureka client`, exposed on `http://eureka-client.oto.tools:8080` and targeting `http://foo.bar`\n* Search and add the `Internal Eureka server` plugin \n* Edit the plugin and choose your eureka server and your app (in our case, `Eureka Server` and `FOO_APP` respectively)\n* Save your route\n\nNow try to call the new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n````\n\nIf everything is working, you should get the port 8085 as the response.The setup is working as expected, but we can improve him by scaling our eureka client.\n\nOpen a new tab in your terminal and run the following command.\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8083\n````\n\nJust wait a few seconds and retry to call your new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\n````\n\nThe configuration is ready and the setup is working, Otoroshi use all instances of your app to dispatch clients on it.\n\n### Connect to an external Eureka server\n\nOtoroshi has the possibility to discover services by connecting to an Eureka Server.\n\nLet's create a route with an Eureka application as Otoroshi target:\n\n* Create a new blank API route\n* Search and add the `External Eureka Server` plugin\n* Set your eureka URL\n* Click on `Fetch Services` button to discover the applications of the Eureka instance\n* In the appeared selector, choose the application to target\n* Once the frontend configured, save your route and try to call it.\n\nWell done, you have exposed your Eureka application through the Otoroshi discovery services.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"name":"zip-backend-plugin.md","id":"/how-to-s/zip-backend-plugin.md","url":"/how-to-s/zip-backend-plugin.html","title":"Quickly expose a website and static files ","content":"# Quickly expose a website and static files \n\n@@include[badge.md](../includes/badge.md) { #badge }\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create an archive with HTML and CSS files](#create-an-archive-with-html-and-css-files)\n2. [Use the Zip Backend Plugin](#use-the-zip-backend-plugin)\n\nAfter completing these steps, you will be able to statically expose any kind of files from an archive.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create an archive with HTML and CSS files\n\nLet's start by creating an archive composed of html and css files.\n\nThe contents of your `index.html` file should be likes this:\n\n```html\n\n\n\n Wasmo plugin\n \n\n\n

      Hello from Wasmo

      \n\n\n```\n\nThe contents of your `index.css` file should be likes this:\n\n```css\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nOnce created, you can create the archive of both.\n\n```sh\nzip bundle.zip index.html index.css\n```\n\n## Use the Zip Backend Plugin \n\nLet's create the route using the Otoroshi admin API. The route content is pretty simple, a few fields about the name and the frontend, and the Zip Backend plugin in the plugins list.\n\nDon't forget to change the default `path-to-the-zip-file` with your path.\n\n``` sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"demootoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"debug\": false,\n \"plugin\": \"cp:otoroshi.next.plugins.ZipFileBackend\",\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"url\": \"file:///bundle.zip\",\n \"headers\": {},\n \"dir\": \"./zips\",\n \"prefix\": null,\n \"ttl\": 3600000\n }\n }\n ]\n}\nEOF\n```\n\nCalling the route in a new browser tab at `http://demo-otoroshi.oto.tools:8080/`. You should see something like the following image:\n\n@@@ div { .centered-img }\n\n@@@\n\nAs we can see, the content of the archive is available, our HTML page is served and the CSS, linked into the HTML page, has loaded.\n\nYou can check this behaviour by calling the following path: \n\n```bash\ncurl http://demo-otoroshi.oto.tools:8080/index.css -v\n```\n\nThe result should be like:\n\n```bash\n< HTTP/1.1 200 OK\n< Transfer-Encoding: chunked\n< Content-Type: text/css\n<\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nCongratulations - You have just exposed your first archive. Do not hesitate to expose any type of content."},{"name":"badge.md","id":"/includes/badge.md","url":"/includes/badge.html","title":"","content":"\n
      \nRoute plugins:\n
      Zip file backend plugin
      \n
      \n\n"},{"name":"experimental.md","id":"/includes/experimental.md","url":"/includes/experimental.html","title":"@@@ warning","content":"@@@ warning\n\nthis feature is **EXPERIMENTAL** and might not work as expected.
      \nIf you encounter any bugs, [please fill an issue](https://github.com/MAIF/otoroshi/issues/new), it will help us a lot :)\n\n@@@\n"},{"name":"fetch-and-start.md","id":"/includes/fetch-and-start.md","url":"/includes/fetch-and-start.html","title":"","content":"\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n"},{"name":"initialize.md","id":"/includes/initialize.md","url":"/includes/initialize.html","title":"","content":"\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
      \nSet up an Otoroshi\n\n
      \n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n"},{"name":"index.md","id":"/index.md","url":"/index.html","title":"Otoroshi","content":"# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.14.0\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n"},{"name":"get-otoroshi.md","id":"/install/get-otoroshi.md","url":"/install/get-otoroshi.html","title":"Get Otoroshi","content":"# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi-16.14.0.zip\nunzip ./otoroshi-16.14.0.zip\ncd otoroshi-16.14.0\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.14.0-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n"},{"name":"index.md","id":"/install/index.md","url":"/install/index.html","title":"Install","content":"# Install\n\nIn this sections, you will find informations about how to install and run Otoroshi\n\n* @ref:[Get Otoroshi](./get-otoroshi.md)\n* @ref:[Setup Otoroshi](./setup-otoroshi.md)\n* @ref:[Run Otoroshi](./run-otoroshi.md)\n\n@@@ index\n\n* [Get Otoroshi](./get-otoroshi.md)\n* [Setup Otoroshi](./setup-otoroshi.md)\n* [Run Otoroshi](./run-otoroshi.md)\n\n@@@\n"},{"name":"run-otoroshi.md","id":"/install/run-otoroshi.md","url":"/install/run-otoroshi.html","title":"Run Otoroshi","content":"# Run Otoroshi\n\nNow you are ready to run Otoroshi. You can run the following command with some tweaks depending on the way you want to configure Otoroshi. If you want to pass a custom configuration file, use the `-Dconfig.file=/path/to/file.conf` flag in the following commands.\n\n## From .zip file\n\n```sh\ncd otoroshi-vx.x.x\n./bin/otoroshi\n```\n\n## From .jar file\n\nFor Java 11\n\n```sh\njava -jar otoroshi.jar\n```\n\nif you want to run the jar file for on a JDK above JDK11, you'll have to add the following flags\n\n```sh\njava \\\n --add-opens=java.base/javax.net.ssl=ALL-UNNAMED \\\n --add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED \\\n --add-exports=java.base/sun.security.x509=ALL-UNNAMED \\\n --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \\\n -Dlog4j2.formatMsgNoLookups=true \\\n -jar otoroshi.jar\n```\n\n## From docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi\n```\n\nYou can also pass useful args like :\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi -Dconfig.file=/usr/app/otoroshi/conf/otoroshi.conf -Dlogger.file=/usr/app/otoroshi/conf/otoroshi.xml\n```\n\nIf you want to provide your own config file, you can read @ref:[the documentation about config files](./setup-otoroshi.md).\n\nYou can also provide some ENV variable using the `--env` flag to customize your Otoroshi instance.\n\nThe list of possible env variables is available @ref:[here](./setup-otoroshi.md).\n\nYou can use a volume to provide configuration like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/conf\" maif/otoroshi\n```\n\nYou can also use a volume if you choose to use `filedb` datastore like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd)/filedb:/usr/app/otoroshi/filedb\" maif/otoroshi -Dotoroshi.storage=file\n```\n\nYou can also use a volume if you choose to use exports files :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/imports\" maif/otoroshi -Dotoroshi.importFrom=/usr/app/otoroshi/imports/export.json\n```\n\n## Run examples\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -Dotoroshi.importFrom=/home/user/otoroshi.json \\\n -Dconfig.file=/home/user/otoroshi.conf \\\n -jar ./otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - Importing from: /home/user/otoroshi.json\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n\nIf you choose to start Otoroshi without importing existing data, Otoroshi will create a new admin user and print the login details in the log. When you will log into the admin dashboard, Otoroshi will ask you to create another account to avoid security issues.\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -jar otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / HHUsiF2UC3OPdmg0lGngEv3RrbIwWV5W\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n"},{"name":"setup-otoroshi.md","id":"/install/setup-otoroshi.md","url":"/install/setup-otoroshi.html","title":"Setup Otoroshi","content":"# Setup Otoroshi\n\nin this section we are going to configure otoroshi before running it for the first time\n\n## Setup the database\n\nRight now, Otoroshi supports multiple datastore. You can choose one datastore over another depending on your use case.\n\n@@@div { .plugin .platform } \n
      Redis
      \n\n
      Recommended
      \n\nThe **redis** datastore is quite nice when you want to easily deploy several Otoroshi instances.\n\n\n\n@link:[Documentation](https://redis.io/topics/quickstart)\n@@@\n\n@@@div { .plugin .platform } \n
      In memory
      \n\nThe **in-memory** datastore is kind of interesting. It can be used for testing purposes, but it is also a good candidate for production because of its fastness.\n\n\n\n@ref:[Start with](../getting-started.md)\n@@@\n\n@@@div { .plugin .platform } \n
      Cassandra
      \n\n
      Clustering
      \n\nExperimental support, should be used in cluster mode for leaders\n\n\n\n@link:[Documentation](https://cassandra.apache.org/doc/latest/cassandra/getting_started/installing.html)\n@@@\n\n@@@div { .plugin .platform } \n
      Postgresql
      \n\n
      Clustering
      \n\nOr any postgresql compatible databse like cockroachdb for instance (experimental support, should be used in cluster mode for leaders)\n\n\n\n@link:[Documentation](https://www.postgresql.org/docs/10/tutorial-install.html)\n@@@\n\n@@@div { .plugin .platform } \n\n
      FileDB
      \n\nThe **filedb** datastore is pretty handy for testing purposes, but is not supposed to be used in production mode. \nNot suitable for production usage.\n\n\n\n@@@\n\n\n@@@ div { .centered-img }\n\n@@@\n\nthe first thing to setup is what kind of datastore you want to use with the `otoroshi.storage` setting\n\n```conf\notoroshi {\n storage = \"inmemory\" # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n}\n```\n\ndepending on the value you chose, you will be able to configure your datastore with the following configuration\n\ninmemory\n: @@snip [inmemory.conf](../snippets/datastores/inmemory.conf) \n\nfile\n: @@snip [file.conf](../snippets/datastores/file.conf) \n\nhttp\n: @@snip [http.conf](../snippets/datastores/http.conf) \n\ns3\n: @@snip [s3.conf](../snippets/datastores/s3.conf) \n\nredis\n: @@snip [lettuce.conf](../snippets/datastores/lettuce.conf) \n\npostgresql\n: @@snip [pg.conf](../snippets/datastores/pg.conf) \n\ncassandra\n: @@snip [inmemory.conf](../snippets/datastores/cassandra.conf) \n\n## Setup your hosts before running\n\nBy default, Otoroshi starts with domain `oto.tools` that automatically targets `127.0.0.1` with no changes to your `/etc/hosts` file. Of course you can change the domain value, you have to add the values in your `/etc/hosts` file according to the setting you put in Otoroshi configuration or define the right ip address at the DNS provider level\n\n* `otoroshi.domain` => `mydomain.org`\n* `otoroshi.backoffice.subdomain` => `otoroshi`\n* `otoroshi.privateapps.subdomain` => `privateapps`\n* `otoroshi.adminapi.exposedSubdomain` => `otoroshi-api`\n* `otoroshi.adminapi.targetSubdomain` => `otoroshi-admin-internal-api`\n\nfor instance if you want to change the default domain and use something like `otoroshi.mydomain.org`, then start otoroshi like \n\n```sh\njava -Dotoroshi.domain=mydomain.org -jar otoroshi.jar\n```\n\n@@@ warning\nOtoroshi cannot be accessed using `http://127.0.0.1:8080` or `http://localhost:8080` because Otoroshi uses Otoroshi to serve it's own UI and API. When otoroshi starts with an empty database, it will create a service descriptor for that using `otoroshi.domain` and the settings listed on this page and in the here that serve Otoroshi API and UI on `http://otoroshi-api.${otoroshi.domain}` and `http://otoroshi.${otoroshi.domain}`.\nOnce the descriptor is saved in database, if you want to change `otoroshi.domain`, you'll have to edit the descriptor in the database or restart Otoroshi with an empty database.\n@@@\n\n@@@ warning\nif your otoroshi instance runs behind a reverse proxy (L4 / L7) or inside a docker container where exposed ports (that you will use to access otoroshi) are not the same that the ones configured in otoroshi (`http.port` and `https.port`), you'll have to configure otoroshi exposed port to avoid bad redirection URLs when using authentication modules and other otoroshi tools. To do that, just set the values of the exposed ports in `otoroshi.exposed-ports.http = $theExposedHttpPort` (OTOROSHI_EXPOSED_PORTS_HTTP) and `otoroshi.exposed-ports.https = $theExposedHttpsPort` (OTOROSHI_EXPOSED_PORTS_HTTPS)\n@@@\n\n## Setup your configuration file\n\nThere is a lot of things you can configure in Otoroshi. By default, Otoroshi provides a configuration that should be enough for testing purpose. But you'll likely need to update this configuration when you'll need to move into production.\n\nIn this page, any configuration property can be set at runtime using a `-D` flag when launching Otoroshi like \n\n```sh\njava -Dhttp.port=8080 -jar otoroshi.jar\n```\n\nor\n\n```sh\n./bin/otoroshi -Dhttp.port=8080 \n```\n\nif you want to define your own config file and use it on an otoroshi instance, use the following flag\n\n```sh\njava -Dconfig.file=/path/to/otoroshi.conf -jar otoroshi.jar\n``` \n\n### Example of a custom. configuration file\n\n```conf\ninclude \"application.conf\"\n\nhttp.port = 8080\n\napp {\n storage = \"inmemory\"\n importFrom = \"./my-state.json\"\n env = \"prod\"\n domain = \"oto.tools\"\n rootScheme = \"http\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n privateapps {\n subdomain = \"privateapps\"\n session {\n exp = 86400000\n }\n }\n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-api-apikey-id\"\n backOfficeApiKeyClientSecret = \"admin-api-apikey-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n }\n claim {\n sharedKey = \"mysecret\"\n }\n filedb {\n path = \"./filedb/state.ndjson\"\n }\n}\n\nplay.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2592000000\n domain = \".oto.tools\"\n cookieName = \"oto-sess\"\n }\n}\n```\n\n### Reference configuration\n\n@@snip [reference.conf](../snippets/reference.conf) \n\n### More config. options\n\nSee default configuration at\n\n* @link:[Base configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/base.conf) { open=new }\n* @link:[Application configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/application.conf) { open=new }\n\n## Configuration with env. variables\n\nEevery property in the configuration file can be overriden by an environment variable if it has env variable override written like `${?ENV_VARIABLE}`).\n\n## Reference configuration for env. variables\n\n@@snip [reference-env.conf](../snippets/reference-env.conf) \n"},{"name":"built-in-legacy-plugins.md","id":"/plugins/built-in-legacy-plugins.md","url":"/plugins/built-in-legacy-plugins.html","title":"Built-in legacy plugins","content":"# Built-in legacy plugins\n\nOtoroshi provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLog }\n\n## Access log (CLF)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in CLF format.\n\nLog format is the following:\n\n`\"$service\" $clientAddress - \"$userId\" [$timestamp] \"$host $method $path $protocol\" \"$status $statusTxt\" $size $snowflake \"$to\" \"$referer\" \"$userAgent\" $http $duration $errorMsg`\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLogJson }\n\n## Access log (JSON)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in json format.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.KafkaAccessLog }\n\n## Kafka access log\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `KafkaAccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged as an event in a kafka topic.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"KafkaAccessLog\": {\n \"enabled\": true,\n \"topic\": \"otoroshi-access-log\",\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KafkaAccessLog\" : {\n \"enabled\" : true,\n \"topic\" : \"otoroshi-access-log\",\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.BasicAuthCaller }\n\n## Basic Auth. caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\nThis plugin accepts the following configuration\n\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.OAuth2Caller }\n\n## OAuth2 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\nThis plugin accepts the following configuration\n\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.cache.ResponseCache }\n\n## Response Cache\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ResponseCache\": {\n \"enabled\": true, // enabled cache\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"autoClean\": true, // cleanup older keys when all bigger than maxSize\n \"filter\": { // cache only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ResponseCache\" : {\n \"enabled\" : true,\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.clientcert.ClientCertChainHeader }\n\n## Client certificate header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ClientCertChain`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ClientCertChain\": {\n \"pem\": { // send client cert as PEM format in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-Pem\"\n },\n \"dns\": { // send JSON array of DNs in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-DNs\"\n },\n \"chain\": { // send JSON representation of client cert chain in a header\n \"send\": true,\n \"header\": \"X-Client-Cert-Chain\"\n },\n \"claims\": { // pass JSON representation of client cert chain in the otoroshi JWT token\n \"send\": false,\n \"name\": \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCertChain\" : {\n \"pem\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-Pem\"\n },\n \"dns\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-DNs\"\n },\n \"chain\" : {\n \"send\" : true,\n \"header\" : \"X-Client-Cert-Chain\"\n },\n \"claims\" : {\n \"send\" : false,\n \"name\" : \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.defer.DeferPlugin }\n\n## Defer Responses\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"DeferPlugin\": {\n \"defaultDefer\": 0 // default defer in millis\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"DeferPlugin\" : {\n \"defaultDefer\" : 0\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.discovery.DiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoHeader }\n\n## Geolocation header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `GeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfoHeader\": {\n \"headerName\": \"X-Geolocation-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfoHeader\" : {\n \"headerName\" : \"X-Geolocation-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.hmac.HMACCallerPlugin }\n\n## HMAC caller plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HMACCallerPlugin`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACCallerPlugin\" : {\n \"secret\" : \"my-defaut-secret\",\n \"algo\" : \"HMAC-SHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiCanary }\n\n## Izanami Canary Campaign\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiCanary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiProxy }\n\n## Izanami APIs Proxy\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiProxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jq.JqBodyTransformer }\n\n## JQ bodies transformer\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `JqBodyTransformer`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\nSome JSON variables are accessible by default :\n\n * `$url`: the request url\n * `$path`: the request path\n * `$domain`: the request domain\n * `$method`: the request method\n * `$headers`: the current request headers (with name in lowercase)\n * `$queryParams`: the current request query params\n * `$otoToken`: the otoroshi protocol token (if one)\n * `$inToken`: the first matched JWT token as is (from verifiers, if one)\n * `$token`: the first matched JWT token as is (from verifiers, if one)\n * `$user`: the current user (if one)\n * `$apikey`: the current apikey (if one)\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jsoup.HtmlPatcher }\n\n## Html Patcher\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"HtmlPatcher\" : {\n \"appendHead\" : [ ],\n \"appendBody\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.log4j.Log4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `Log4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them.\n\nThis plugin can accept the following configuration\n\n```javascript\n{\n \"Log4ShellFilter\": {\n \"status\": 200, // the status send back when an attack expression is found\n \"body\": \"\", // the body send back when an attack expression is found\n \"parseBody\": false // enables request body parsing to find attack expression\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"Log4ShellFilter\" : {\n \"status\" : 200,\n \"body\" : \"\",\n \"parseBody\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.loggers.BodyLogger }\n\n## Body logger\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BodyLogger`\n\n### Description\n\nThis plugin can log body present in request and response. It can just logs it, store in in the redis store with a ttl and send it to analytics.\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"BodyLogger\": {\n \"enabled\": true, // enabled logging\n \"log\": true, // just log it\n \"store\": false, // store bodies in datastore\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"sendToAnalytics\": false, // send bodies to analytics\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"password\": \"password\", // password for the ui, if none, it's public\n \"filter\": { // log only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"BodyLogger\" : {\n \"enabled\" : true,\n \"log\" : true,\n \"store\" : false,\n \"ttl\" : 300000,\n \"sendToAnalytics\" : false,\n \"maxSize\" : 5242880,\n \"password\" : \"password\",\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.mirror.MirroringPlugin }\n\n## Mirroring plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `MirroringPlugin`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"MirroringPlugin\": {\n \"enabled\": true, // enabled mirroring\n \"to\": \"https://foo.bar.dev\", // the url of the service to mirror\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"MirroringPlugin\" : {\n \"enabled\" : true,\n \"to\" : \"https://foo.bar.dev\",\n \"captureResponse\" : false,\n \"generateEvents\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oauth1.OAuth1CallerPlugin }\n\n## OAuth1 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"OAuth1Caller\" : {\n \"algo\" : \"HmacSHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oidc.OIDCHeaders }\n\n## OIDC headers\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCHeaders\" : {\n \"profile\" : {\n \"send\" : true,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idtoken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accesstoken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.security.SecurityTxt }\n\n## Security Txt\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `SecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"SecurityTxt\": {\n \"Contact\": \"contact@foo.bar\", // mandatory, a link or e-mail address for people to contact you about security issues\n \"Encryption\": \"http://url-to-public-key\", // optional, a link to a key which security researchers should use to securely talk to you\n \"Acknowledgments\": \"http://url\", // optional, a link to a web page where you say thank you to security researchers who have helped you\n \"Preferred-Languages\": \"en, fr, es\", // optional\n \"Policy\": \"http://url\", // optional, a link to a policy detailing what security researchers should do when searching for or reporting security issues\n \"Hiring\": \"http://url\", // optional, a link to any security-related job openings in your organisation\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"SecurityTxt\" : {\n \"Contact\" : \"contact@foo.bar\",\n \"Encryption\" : \"https://...\",\n \"Acknowledgments\" : \"https://...\",\n \"Preferred-Languages\" : \"en, fr\",\n \"Policy\" : \"https://...\",\n \"Hiring\" : \"https://...\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.static.StaticResponse }\n\n## Static Response\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `StaticResponse`\n\n### Description\n\nThis plugin returns a static response for any request\n\n\n\n### Default configuration\n\n```json\n{\n \"StaticResponse\" : {\n \"status\" : 200,\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n },\n \"body\" : \"{\\\"message\\\":\\\"hello world!\\\"}\",\n \"bodyBase64\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/user-agent`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoHeader }\n\n## User-Agent header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `UserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfoHeader\": {\n \"headerName\": \"X-User-Agent-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfoHeader\" : {\n \"headerName\" : \"X-User-Agent-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.workflow.WorkflowEndpoint }\n\n## [DEPRECATED] Workflow endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `WorkflowEndpoint`\n\n### Description\n\nThis plugin runs a workflow and return the response\n\n\n\n### Default configuration\n\n```json\n{\n \"WorkflowEndpoint\" : {\n \"workflow\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.biscuit.BiscuitValidator }\n\n## Biscuit token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nThis plugin validates a Biscuit token.\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nexpected response from http service is\n\n```json\n{\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n}\n```\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"url\": \"...\", // url for the call\n \"headers\": {}, // http header for the call\n \"ttl\": 600000, // cache ttl,\n \"mtlsConfig\": {\n \"certId\": \"xxxxx\",\n \"mtls\": false,\n \"loose\": false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingHttpValidator\" : {\n \"url\" : \"http://foo.bar\",\n \"ttl\" : 600000,\n \"headers\" : { },\n \"mtlsConfig\" : {\n \"certId\" : \"...\",\n \"mtls\" : false,\n \"loose\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingValidator }\n\n## Client certificate matching\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\" : {\n \"serialNumbers\" : [ ],\n \"subjectDNs\" : [ ],\n \"issuerDNs\" : [ ],\n \"regexSubjectDNs\" : [ ],\n \"regexIssuerDNs\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertValidator }\n\n## Client Certificate Only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.hmac.HMACValidator }\n\n## HMAC access validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HMACAccessValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACAccessValidator\" : {\n \"secret\" : \"\"\n }\n}\n```\n\n\n\n### Documentation\n\n\n The HMAC signature needs to be set on the `Authorization` or `Proxy-Authorization` header.\n The format of this header should be : `hmac algorithm=\"\", headers=\"
      \", signature=\"\"`\n As example, a simple nodeJS call with the expected header\n ```js\n const crypto = require('crypto');\n const fetch = require('node-fetch');\n\n const date = new Date()\n const secret = \"my-secret\" // equal to the api key secret by default\n\n const algo = \"sha512\"\n const signature = crypto.createHmac(algo, secret)\n .update(date.getTime().toString())\n .digest('base64');\n\n fetch('http://myservice.oto.tools:9999/api/test', {\n headers: {\n \"Otoroshi-Client-Id\": \"my-id\",\n \"Otoroshi-Client-Secret\": \"my-secret\",\n \"Date\": date.getTime().toString(),\n \"Authorization\": `hmac algorithm=\"hmac-${algo}\", headers=\"Date\", signature=\"${signature}\"`,\n \"Accept\": \"application/json\"\n }\n })\n .then(r => r.json())\n .then(console.log)\n ```\n In this example, we have an Otoroshi service deployed on http://myservice.oto.tools:9999/api/test, protected by api keys.\n The secret used is the secret of the api key (by default, but you can change it and define a secret on the plugin configuration).\n We send the base64 encoded date of the day, signed by the secret, in the Authorization header. We specify the headers signed and the type of algorithm used.\n You can sign more than one header but you have to list them in the headers fields (each one separate by a space, example : headers=\"Date KeyId\").\n The algorithm used can be HMAC-SHA1, HMAC-SHA256, HMAC-SHA384 or HMAC-SHA512.\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.oidc.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.quotas.ServiceQuotas }\n\n## Public quotas\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `ServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current service\n\n\n\n\n\n\n\n### Default configuration\n\n```json\n{\n \"ServiceQuotas\" : {\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.users.HasAllowedUsersValidator }\n\n## Allowed users only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasAllowedUsersValidator\": {\n \"usernames\": [], // allowed usernames\n \"emails\": [], // allowed user email addresses\n \"emailDomains\": [], // allowed user email domains\n \"metadataMatch\": [], // json path expressions to match against user metadata. passes if one match\n \"metadataNotMatch\": [], // json path expressions to match against user metadata. passes if none match\n \"profileMatch\": [], // json path expressions to match against user profile. passes if one match\n \"profileNotMatch\": [], // json path expressions to match against user profile. passes if none match\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasAllowedUsersValidator\" : {\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"emailDomains\" : [ ],\n \"metadataMatch\" : [ ],\n \"metadataNotMatch\" : [ ],\n \"profileMatch\" : [ ],\n \"profileNotMatch\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ApikeyAuthModule }\n\n## Apikey auth module\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"ApikeyAuthModule\" : {\n \"realm\" : \"apikey-auth-module-realm\",\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : [ ],\n \"oneMetaIn\" : [ ],\n \"allMetaIn\" : [ ],\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.CertificateAsApikey }\n\n## Client certificate as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `CertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"CertificateAsApikey\" : {\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ClientCredentialFlowExtractor }\n\n## Client Credential Flow ApiKey extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin can extract an apikey from an opaque access_token generate by the `ClientCredentialFlow` plugin\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.biscuit.BiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.discovery.DiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.IpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"apikey\": \"xxxxxxx\",\n \"timeout\": 2000, // timeout in ms\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"apikey\" : \"xxxxxxx\",\n \"timeout\" : 2000,\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.MaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"path\": \"/foo/bar/cities.mmdb\", // file path, can be \"global\"\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"path\" : \"global\",\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.jwt.JwtUserExtractor }\n\n## Jwt user extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `JwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"JwtUserExtractor\" : {\n \"verifier\" : \"\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\" : \"email\",\n \"metaPath\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.oidc.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenAsApikey\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.useragent.UserAgentExtractor }\n\n## User-Agent details extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `UserAgentInfo`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfo\": {\n \"log\": false // will log user-agent details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfo\" : {\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.apikeys.ClientCredentialService }\n\n## Client Credential Service\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `ClientCredentialService`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.discovery.DiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator }\n\n## Kubernetes admission validator webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to handle manifests validation\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector }\n\n## Kubernetes sidecar injector webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to inject otoroshi-sidecar in pods\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.jobs.StateExporter }\n\n## Otoroshi state exporter job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `StateExporter`\n\n### Description\n\nThis job send an event containing the full otoroshi export every n seconds\n\n\n\n### Default configuration\n\n```json\n{\n \"StateExporter\" : {\n \"every_sec\" : 3600,\n \"format\" : \"json\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleCertificatesFetcherJob }\n\n## Tailscale certificate fetcher job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will fetch certificates from Tailscale ACME provider\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleTargetsJob }\n\n## Tailscale targets job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will aggregates Tailscale possible online targets\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesIngressControllerJob }\n\n## Kubernetes Ingress Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi as an Ingress Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesOtoroshiCRDsControllerJob }\n\n## Kubernetes Otoroshi CRDs Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi CRDs Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesToOtoroshiCertSyncJob }\n\n## Kubernetes to Otoroshi certs. synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. TLS secrets from Kubernetes to Otoroshi\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.OtoroshiToKubernetesCertSyncJob }\n\n## Otoroshi certs. to Kubernetes secrets synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. Otoroshi certs to Kubernetes TLS secrets\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.wasm.WasmVmPoolCleaner }\n\n## otoroshi.wasm.WasmVmPoolCleaner\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.next.proxy.ProxyEngine }\n\n## Otoroshi next proxy engine (experimental)\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `NextGenProxyEngine`\n\n### Description\n\nThis plugin holds the next generation otoroshi proxy engine implementation. This engine is **experimental** and may not work as expected !\n\nYou can active this plugin only on some domain names so you can easily A/B test the new engine.\nThe new proxy engine is designed to be more reactive and more efficient generally.\nIt is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nThe idea is to only rely on plugins to work and avoid losing time with features that are not used in service descriptors.\nAn automated conversion happens for every service descriptor. If the exposed domain is handled by this plugin, it will be served by this plugin.\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `route-compositions`: multiple routing rules based on hostname, path, method and headers that will execute the same list of plugins\n - `backends`: a list of targets to contact a backend\n\nas an example, let say you want to use the new engine on your service exposed on `api.foo.bar/api`.\nTo do that, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration,\nenabled it and in `domains` add the value `api.foo.bar` (it is possible to use `*.foo.bar` if that's what you want to do).\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the old one.\n\n\n\n### Default configuration\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"domains\" : [ \"*\" ],\n \"deny_domains\" : [ ],\n \"reporting\" : true,\n \"merge_sync_steps\" : true,\n \"export_reporting\" : false,\n \"apply_legacy_checks\" : true,\n \"debug\" : false,\n \"capture\" : false,\n \"captureMaxEntitySize\" : 4194304,\n \"debug_headers\" : false,\n \"routing_strategy\" : \"tree\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.script.ForwardTrafficHandler }\n\n## Forward traffic\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `ForwardTrafficHandler`\n\n### Description\n\nThis plugin can be use to perform a raw traffic forward to an URL without passing through otoroshi routing\n\n\n\n### Default configuration\n\n```json\n{\n \"ForwardTrafficHandler\" : {\n \"domains\" : {\n \"my.domain.tld\" : {\n \"baseUrl\" : \"https://my.otherdomain.tld\",\n \"secret\" : \"jwt signing secret\",\n \"service\" : {\n \"id\" : \"service id for analytics\",\n \"name\" : \"service name for analytics\"\n }\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n\n\n"},{"name":"built-in-plugins.md","id":"/plugins/built-in-plugins.md","url":"/plugins/built-in-plugins.html","title":"Built-in plugins","content":"# Built-in plugins\n\nOtoroshi next provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration.\n\n
      \n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersIn }\n\n## Additional headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersIn`\n\n### Description\n\nThis plugin adds headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersOut }\n\n## Additional headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersOut`\n\n### Description\n\nThis plugin adds headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AllowHttpMethods }\n\n## Allowed HTTP methods\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AllowHttpMethods`\n\n### Description\n\nThis plugin verifies the current request only uses allowed http methods\n\n\n\n### Default configuration\n\n```json\n{\n \"allowed\" : [ ],\n \"forbidden\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyAuthModule }\n\n## Apikey auth module\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"realm\" : \"apikey-auth-module-realm\",\n \"matcher\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyCalls }\n\n## Apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyCalls`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass\n\n\n\n### Default configuration\n\n```json\n{\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyQuotas }\n\n## Apikey quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyQuotas`\n\n### Description\n\nIncrements quotas for the currents apikey. Useful when 'legacy checks' are disabled on a service/globally or when apikey are extracted in a custom fashion.\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AuthModule }\n\n## Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AuthModule`\n\n### Description\n\nThis plugin applies an authentication module\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BasicAuthCaller }\n\n## Basic Auth. caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : null,\n \"passaword\" : null,\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BrotliResponseCompressor }\n\n## Brotli compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BrotliResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using brotli\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BuildMode }\n\n## Build mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BuildMode`\n\n### Description\n\nThis plugin displays a build page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.CanaryMode }\n\n## Canary mode\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.CanaryMode`\n\n### Description\n\nThis plugin can split a portion of the traffic to canary backends\n\n\n\n### Default configuration\n\n```json\n{\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ContextValidation }\n\n## Context validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ContextValidation`\n\n### Description\n\nThis plugin validates the current context using JSONPath validators.\n\nThis plugin let you configure a list of validators that will check if the current call can pass.\nA validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value.\nThe JSONPath will be applied on a document that will look like\n\n```js\n{\n \"snowflake\" : \"1516772930422308903\",\n \"apikey\" : { // current apikey\n \"clientId\" : \"vrmElDerycXrofar\",\n \"clientName\" : \"default-apikey\",\n \"metadata\" : {\n \"foo\" : \"bar\"\n },\n \"tags\" : [ ]\n },\n \"user\" : null, // current user\n \"request\" : {\n \"id\" : 1,\n \"method\" : \"GET\",\n \"headers\" : {\n \"Host\" : \"ctx-validation-next-gen.oto.tools:9999\",\n \"Accept\" : \"*/*\",\n \"User-Agent\" : \"curl/7.64.1\",\n \"Authorization\" : \"Basic dnJtRWxEZXJ5Y1hyb2ZhcjpvdDdOSTkyVGI2Q2J4bWVMYU9UNzJxamdCU2JlRHNLbkxtY1FBcXBjVjZTejh0Z3I1b2RUOHAzYjB5SEVNRzhZ\",\n \"Remote-Address\" : \"127.0.0.1:58929\",\n \"Timeout-Access\" : \"\",\n \"Raw-Request-URI\" : \"/foo\",\n \"Tls-Session-Info\" : \"Session(1650461821330|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\" : [ ],\n \"tls\" : false,\n \"uri\" : \"/foo\",\n \"path\" : \"/foo\",\n \"version\" : \"HTTP/1.1\",\n \"has_body\" : false,\n \"remote\" : \"127.0.0.1\",\n \"client_cert_chain\" : null\n },\n \"config\" : {\n \"validators\" : [ {\n \"path\" : \"$.apikey.metadata.foo\",\n \"value\" : \"bar\"\n } ]\n },\n \"global_config\" : { ... }, // global config\n \"attrs\" : {\n \"otoroshi.core.SnowFlake\" : \"1516772930422308903\",\n \"otoroshi.core.ElCtx\" : {\n \"requestId\" : \"1516772930422308903\",\n \"requestSnowflake\" : \"1516772930422308903\",\n \"requestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\"\n },\n \"otoroshi.next.core.Report\" : \"otoroshi.next.proxy.NgExecutionReport@277b44e2\",\n \"otoroshi.core.RequestStart\" : 1650461821545,\n \"otoroshi.core.RequestWebsocket\" : false,\n \"otoroshi.core.RequestCounterOut\" : 0,\n \"otoroshi.core.RemainingQuotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 0,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 2,\n \"remainingCallsPerDay\" : 9999998,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 269,\n \"remainingCallsPerMonth\" : 9999731\n },\n \"otoroshi.next.core.MatchedRoutes\" : \"MutableList(route_022825450-e97d-42ed-8e22-b23342c1c7c8)\",\n \"otoroshi.core.RequestNumber\" : 1,\n \"otoroshi.next.core.Route\" : { ... }, // current route as json\n \"otoroshi.core.RequestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\",\n \"otoroshi.core.ApiKey\" : { ... }, // current apikey as json\n \"otoroshi.core.User\" : { ... }, // current user as json\n \"otoroshi.core.RequestCounterIn\" : 0\n },\n \"route\" : { ... },\n \"token\" : null // current valid jwt token if one\n}\n```\n\nthe expected value support some syntax tricks like\n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current apikey has a metadata name `foo` with a value containing `bar`, you can write the following validator\n\n```js\n{\n \"path\": \"$.apikey.metadata.foo\",\n \"value\": \"Contains(bar)\"\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"validators\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Cors }\n\n## CORS\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Cors`\n\n### Description\n\nThis plugin applies CORS rules\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_origin\" : \"*\",\n \"expose_headers\" : [ ],\n \"allow_headers\" : [ ],\n \"allow_methods\" : [ ],\n \"excluded_patterns\" : [ ],\n \"max_age\" : null,\n \"allow_credentials\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.DisableHttp10 }\n\n## Disable HTTP/1.0\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.DisableHttp10`\n\n### Description\n\nThis plugin forbids HTTP/1.0 requests\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EndlessHttpResponse }\n\n## Endless HTTP responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EndlessHttpResponse`\n\n### Description\n\nThis plugin returns 128 Gb of 0 to the ip addresses is in the list\n\n\n\n### Default configuration\n\n```json\n{\n \"finger\" : false,\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaServerSink }\n\n## Eureka instance\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaServerSink`\n\n### Description\n\nEureka plugin description\n\n\n\n### Default configuration\n\n```json\n{\n \"evictionTimeout\" : 300\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaTarget }\n\n## Internal Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an internal Eureka server.\n If you want to use a target which it locate outside of Otoroshi, you must use the External Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ExternalEurekaTarget }\n\n## External Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ExternalEurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an external Eureka server.\n If you want to use a target that is directly exposed by an implementation of Eureka by Otoroshi,\n you must use the Internal Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForceHttpsTraffic }\n\n## Force HTTPS traffic\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForceHttpsTraffic`\n\n### Description\n\nThis plugin verifies the current request uses HTTPS\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForwardedHeader }\n\n## Forwarded header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForwardedHeader`\n\n### Description\n\nThis plugin adds all the Forwarded header to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalMaintenanceMode }\n\n## Global Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalMaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page for every services. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalPerIpAddressThrottling }\n\n## Global per ip address throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalPerIpAddressThrottling`\n\n### Description\n\nEnforce global per ip address throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalThrottling }\n\n## Global throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalThrottling`\n\n### Description\n\nEnforce global throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLBackend }\n\n## GraphQL Composer\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLBackend`\n\n### Description\n\nThis plugin exposes a GraphQL API that you can compose with whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"schema\" : \"\\n type User {\\n name: String!\\n firstname: String!\\n }\\n\\n type Query {\\n users: [User] @json(data: \\\"[{ \\\\\\\"firstname\\\\\\\": \\\\\\\"Foo\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Bar\\\\\\\" }, { \\\\\\\"firstname\\\\\\\": \\\\\\\"Bar\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Foo\\\\\\\" }]\\\")\\n }\\n \",\n \"permissions\" : [ ],\n \"initial_data\" : null,\n \"max_depth\" : 15\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLProxy }\n\n## GraphQL Proxy\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLProxy`\n\n### Description\n\nThis plugin can apply validations (query, schema, max depth, max complexity) on graphql endpoints\n\n\n\n### Default configuration\n\n```json\n{\n \"endpoint\" : \"https://countries.trevorblades.com/graphql\",\n \"schema\" : null,\n \"max_depth\" : 50,\n \"max_complexity\" : 50000,\n \"path\" : \"/graphql\",\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLQuery }\n\n## GraphQL Query to REST\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLQuery`\n\n### Description\n\nThis plugin can be used to call GraphQL query endpoints and expose it as a REST endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://some.graphql/endpoint\",\n \"headers\" : { },\n \"method\" : \"POST\",\n \"query\" : \"{\\n\\n}\",\n \"timeout\" : 60000,\n \"response_path\" : null,\n \"response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GzipResponseCompressor }\n\n## Gzip compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GzipResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using gzip\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACCaller }\n\n## HMAC caller plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACCaller`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null,\n \"algo\" : \"HMAC-SHA512\",\n \"authorizationHeader\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACValidator }\n\n## HMAC access validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HeadersValidation }\n\n## Headers validation\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HeadersValidation`\n\n### Description\n\nThis plugin validates the values of incoming request headers\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Http3Switch }\n\n## Http3 traffic switch\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Http3Switch`\n\n### Description\n\nThis plugin injects additional alt-svc header to switch to the http3 server\n\n\n\n### Default configuration\n\n```json\n{\n \"ma\" : 3600\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ImageReplacer }\n\n## Image replacer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ImageReplacer`\n\n### Description\n\nReplace all response with content-type image/* as they are proxied\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://raw.githubusercontent.com/MAIF/otoroshi/master/resources/otoroshi-logo.png\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressAllowedList }\n\n## IP allowed list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressAllowedList`\n\n### Description\n\nThis plugin verifies the current request ip address is in the allowed list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressBlockList }\n\n## IP block list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressBlockList`\n\n### Description\n\nThis plugin verifies the current request ip address is not in the blocked list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQ }\n\n## JQ\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQ`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"request\" : \".\",\n \"response\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQRequest }\n\n## JQ transform request\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQRequest`\n\n### Description\n\nThis plugin let you transform request JSON body using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQResponse }\n\n## JQ transform response\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQResponse`\n\n### Description\n\nThis plugin let you transform JSON response using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlRequest }\n\n## request body json-to-xml\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlRequest`\n\n### Description\n\nThis plugin transform incoming request body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlResponse }\n\n## response body json-to-xml\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlResponse`\n\n### Description\n\nThis plugin transform response body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtSigner }\n\n## Jwt signer\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtSigner`\n\n### Description\n\nThis plugin can only generate token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"replace_if_present\" : true,\n \"fail_if_present\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerification }\n\n## Jwt verifiers\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerification`\n\n### Description\n\nThis plugin verifies the current request with one or more jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifiers\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerificationOnly }\n\n## Jwt verification only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerificationOnly`\n\n### Description\n\nThis plugin verifies the current request with one jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"fail_if_absent\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MaintenanceMode }\n\n## Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersIn }\n\n## Missing headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersIn`\n\n### Description\n\nThis plugin adds headers (if missing) in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersOut }\n\n## Missing headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersOut`\n\n### Description\n\nThis plugin adds headers (if missing) in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MockResponses }\n\n## Mock Responses\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MockResponses`\n\n### Description\n\nThis plugin returns mock responses\n\n\n\n### Default configuration\n\n```json\n{\n \"responses\" : [ ],\n \"pass_through\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MultiAuthModule }\n\n## Multi Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MultiAuthModule`\n\n### Description\n\nThis plugin applies an authentication module from a list of selected modules\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_modules\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleExpectedUser }\n\n## User logged in expected\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleExpectedUser`\n\n### Description\n\nThis plugin enforce that a user from any auth. module is logged in\n\n\n\n### Default configuration\n\n```json\n{\n \"only_from\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleUserExtractor }\n\n## User extraction from auth. module\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleUserExtractor`\n\n### Description\n\nThis plugin extracts users from an authentication module without enforcing login\n\n\n\n### Default configuration\n\n```json\n{\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitExtractor`\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitValidator }\n\n## Biscuit token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitValidator`\n\n### Description\n\nThis plugin validates a Biscuit token\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCertificateAsApikey }\n\n## Client certificate as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"read_only\" : false,\n \"allow_client_id_only\" : false,\n \"throttling_quota\" : 100,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000,\n \"constrained_services_only\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCertChainHeader }\n\n## Client certificate header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCertChainHeader`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers\n\n\n\n### Default configuration\n\n```json\n{\n \"send_pem\" : false,\n \"pem_header_name\" : \"X-Client-Cert-Pem\",\n \"send_dns\" : false,\n \"dns_header_name\" : \"X-Client-Cert-DNs\",\n \"send_chain\" : false,\n \"chain_header_name\" : \"X-Client-Cert-Chain\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentialTokenEndpoint }\n\n## Client credential token endpoint\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentialTokenEndpoint`\n\n### Description\n\nThis plugin provide the endpoint for the client_credential flow token endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentials }\n\n## Client Credential Service\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentials`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\",\n \"domain\" : \"*\",\n \"secure\" : true,\n \"biscuit\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomQuotas }\n\n## Custom quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomQuotas`\n\n### Description\n\nThis plugin will enforce quotas on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomThrottling }\n\n## Custom throttling\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomThrottling`\n\n### Description\n\nThis plugin will enforce throttling on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"throttling_quota\" : 100\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDefaultRequestBody }\n\n## Default request body\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDefaultRequestBody`\n\n### Description\n\nThis plugin adds a default request body if none specified\n\n\n\n### Default configuration\n\n```json\n{\n \"bodyBinary\" : \"\",\n \"contentType\" : \"text/plain\",\n \"contentEncoding\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDeferPlugin }\n\n## Defer Responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\n\n\n### Default configuration\n\n```json\n{\n \"duration\" : 0\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationSink`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoveryTargetsSelector`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgErrorRewriter }\n\n## Error response rewrite\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgErrorRewriter`\n\n### Description\n\nThis plugin catch http response with specific statuses and rewrite the response\n\n\n\n### Default configuration\n\n```json\n{\n \"ranges\" : [ {\n \"from\" : 500,\n \"to\" : 599\n } ],\n \"templates\" : {\n \"default\" : \"\\n \\n

      An error occurred with id: ${error_id}

      \\n

      please contact your administrator with this error id !

      \\n \\n\"\n },\n \"log\" : true,\n \"export\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoEndpoint`\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint `/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoHeader }\n\n## Geolocation header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasAllowedUsersValidator }\n\n## Allowed users only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\n\n\n### Default configuration\n\n```json\n{\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"email_domains\" : [ ],\n \"metadata_match\" : [ ],\n \"metadata_not_match\" : [ ],\n \"profile_match\" : [ ],\n \"profile_not_match\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator`\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following fetched from an http endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingValidator }\n\n## Client certificate matching\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertValidator }\n\n## Client Certificate Only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertValidator`\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHtmlPatcher }\n\n## Html Patcher\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"append_head\" : [ ],\n \"append_body\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHttpClientCache }\n\n## HTTP Client Cache\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHttpClientCache`\n\n### Description\n\nThis plugin add cache headers to responses\n\n\n\n### Default configuration\n\n```json\n{\n \"max_age_seconds\" : 86400,\n \"methods\" : [ \"GET\" ],\n \"status\" : [ 200 ],\n \"mime_types\" : [ \"text/html\" ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"apikey\" : null,\n \"timeout\" : 2000,\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Canary }\n\n## Izanami V1 Canary Campaign\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Canary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test)\n\n\n\n### Default configuration\n\n```json\n{\n \"experiment_id\" : \"foo:bar:qix\",\n \"config_id\" : \"foo:bar:qix:config\",\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 5000,\n \"route_config\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Proxy }\n\n## Izanami v1 APIs Proxy\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Proxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"/api/izanami\",\n \"feature_pattern\" : \"*\",\n \"config_pattern\" : \"*\",\n \"auto_context\" : false,\n \"features_enabled\" : true,\n \"features_with_context_enabled\" : true,\n \"configuration_enabled\" : false,\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 500\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgJwtUserExtractor }\n\n## Jwt user extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgJwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : \"none\",\n \"strict\" : true,\n \"strip\" : false,\n \"name_path\" : null,\n \"email_path\" : null,\n \"meta_path\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyApikeyCall }\n\n## Legacy apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyApikeyCall`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass. This plugin behaves exactly like the service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyAuthModuleCall }\n\n## Legacy Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyAuthModuleCall`\n\n### Description\n\nThis plugin applies an authentication module the same way service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLog4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLog4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"body\" : \"\",\n \"parse_body\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"global\",\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgResponseCache }\n\n## Response Cache\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\n\n\n### Default configuration\n\n```json\n{\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgSecurityTxt }\n\n## Security Txt\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgSecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/)\n\n\n\n### Default configuration\n\n```json\n{\n \"contact\" : \"contact@foo.bar\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgServiceQuotas }\n\n## Public quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current route\n\n\n\n### Default configuration\n\n```json\n{\n \"throttling_quota\" : 10000000,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgTrafficMirroring }\n\n## Traffic Mirroring\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgTrafficMirroring`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\n\n\n### Default configuration\n\n```json\n{\n \"to\" : \"https://foo.bar.dev\",\n \"enabled\" : true,\n \"capture_response\" : false,\n \"generate_events\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentExtractor }\n\n## User-Agent details extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentExtractor`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoEndpoint`\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint: /.well-known/otoroshi/plugins/user-agent\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoHeader }\n\n## User-Agent header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth1Caller }\n\n## OAuth1 caller\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"consumerKey\" : null,\n \"consumerSecret\" : null,\n \"token\" : null,\n \"tokenSecret\" : null,\n \"algo\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth2Caller }\n\n## OAuth2 caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"client_credentials\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : null,\n \"audience\" : null,\n \"user\" : null,\n \"password\" : null,\n \"cacheTokenSeconds\" : 600000,\n \"tlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCHeaders }\n\n## OIDC headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"profile\" : {\n \"send\" : false,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idToken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accessToken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiChallenge }\n\n## Otoroshi challenge token\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiChallenge`\n\n### Description\n\nThis plugin adds a jwt challenge token to the request to a backend and expects a response with a matching token\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"V2\",\n \"ttl\" : 30,\n \"request_header_name\" : null,\n \"response_header_name\" : null,\n \"algo_to_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"algo_from_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"state_resp_leeway\" : 10\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiHeadersIn }\n\n## Otoroshi headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiHeadersIn`\n\n### Description\n\nThis plugin adds Otoroshi specific headers to the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiInfos }\n\n## Otoroshi info. token\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiInfos`\n\n### Description\n\nThis plugin adds a jwt token with informations about the caller to the backend\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"Latest\",\n \"ttl\" : 30,\n \"header_name\" : null,\n \"add_fields\" : null,\n \"algo\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OverrideHost }\n\n## Override host header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OverrideHost`\n\n### Description\n\nThis plugin override the current Host header with the Host of the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.PublicPrivatePaths }\n\n## Public/Private paths\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.PublicPrivatePaths`\n\n### Description\n\nThis plugin allows or forbid request based on path patterns\n\n\n\n### Default configuration\n\n```json\n{\n \"strict\" : false,\n \"private_patterns\" : [ ],\n \"public_patterns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.QueryTransformer }\n\n## Query param transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.QueryTransformer`\n\n### Description\n\nThis plugin can modify the query params of the request\n\n\n\n### Default configuration\n\n```json\n{\n \"remove\" : [ ],\n \"rename\" : { },\n \"add\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RBAC }\n\n## RBAC\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RBAC`\n\n### Description\n\nThis plugin check if current user/apikey/jwt token has the right role\n\n\n\n### Default configuration\n\n```json\n{\n \"allow\" : [ ],\n \"deny\" : [ ],\n \"allow_all\" : false,\n \"deny_all\" : false,\n \"jwt_path\" : null,\n \"apikey_path\" : null,\n \"user_path\" : null,\n \"role_prefix\" : null,\n \"roles\" : \"roles\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ReadOnlyCalls }\n\n## Read only requests\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ReadOnlyCalls`\n\n### Description\n\nThis plugin verifies the current request only reads data\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Redirection }\n\n## Redirection\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Redirection`\n\n### Description\n\nThis plugin redirects the current request elsewhere\n\n\n\n### Default configuration\n\n```json\n{\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersIn }\n\n## Remove headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersIn`\n\n### Description\n\nThis plugin removes headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersOut }\n\n## Remove headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersOut`\n\n### Description\n\nThis plugin removes headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Robots }\n\n## Robots\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Robots`\n\n### Description\n\nThis plugin provides all the necessary tool to handle search engine robots\n\n\n\n### Default configuration\n\n```json\n{\n \"robot_txt_enabled\" : true,\n \"robot_txt_content\" : \"User-agent: *\\nDisallow: /\\n\",\n \"meta_enabled\" : true,\n \"meta_content\" : \"noindex,nofollow,noarchive\",\n \"header_enabled\" : true,\n \"header_content\" : \"noindex, nofollow, noarchive\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RoutingRestrictions }\n\n## Routing Restrictions\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RoutingRestrictions`\n\n### Description\n\nThis plugin apply routing restriction `method domain/path` on the current request/route\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_last\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"not_found\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.S3Backend }\n\n## S3 Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.S3Backend`\n\n### Description\n\nThis plugin is able to S3 bucket with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"bucket\" : \"\",\n \"endpoint\" : \"\",\n \"region\" : \"eu-west-1\",\n \"access\" : \"client\",\n \"secret\" : \"secret\",\n \"key\" : \"\",\n \"chunkSize\" : 8388608,\n \"v4auth\" : true,\n \"writeEvery\" : 60000,\n \"acl\" : \"private\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SOAPAction }\n\n## SOAP action\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SOAPAction`\n\n### Description\n\nThis plugin is able to call SOAP actions and expose it as a rest endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : null,\n \"envelope\" : \"\",\n \"action\" : null,\n \"preserve_query\" : true,\n \"charset\" : null,\n \"jq_request_filter\" : null,\n \"jq_response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SendOtoroshiHeadersBack }\n\n## Send otoroshi headers back\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SendOtoroshiHeadersBack`\n\n### Description\n\nThis plugin adds response header containing useful informations about the current call\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SnowMonkeyChaos }\n\n## Snow Monkey Chaos\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SnowMonkeyChaos`\n\n### Description\n\nThis plugin introduce some chaos into you life\n\n\n\n### Default configuration\n\n```json\n{\n \"large_request_fault\" : null,\n \"large_response_fault\" : null,\n \"latency_injection_fault\" : null,\n \"bad_responses_fault\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticBackend }\n\n## Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticBackend`\n\n### Description\n\nThis plugin is able to serve a static folder with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"root_path\" : \"/tmp\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticResponse }\n\n## Static Response\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticResponse`\n\n### Description\n\nThis plugin returns static responses\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"headers\" : { },\n \"body\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TailscaleSelectTargetByName }\n\n## Tailscale select target by name\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TailscaleSelectTargetByName`\n\n### Description\n\nThis plugin selects a machine instance on Tailscale network based on its name\n\n\n\n### Default configuration\n\n```json\n{\n \"machine_name\" : \"my-machine\",\n \"use_ip_address\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TcpTunnel }\n\n## TCP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TcpTunnel`\n\n### Description\n\nThis plugin creates TCP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.UdpTunnel }\n\n## UDP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.UdpTunnel`\n\n### Description\n\nThis plugin creates UDP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.W3CTracing }\n\n## W3C Trace Context\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.W3CTracing`\n\n### Description\n\nThis plugin propagates W3C Trace Context spans and can export it to Jaeger or Zipkin\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"noop\",\n \"endpoint\" : \"http://localhost:3333/spans\",\n \"timeout\" : 30000,\n \"baggage\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmAccessValidator }\n\n## Wasm Access control\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmAccessValidator`\n\n### Description\n\nDelegate route access to a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmBackend }\n\n## Wasm Backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmBackend`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as backend\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmOPA }\n\n## Open Policy Agent (OPA)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmOPA`\n\n### Description\n\nRepo policies as WASM modules\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : true,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmPreRoute }\n\n## Wasm pre-route\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmPreRoute`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as in pre-route phase\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRequestTransformer }\n\n## Wasm Request Transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRequestTransformer`\n\n### Description\n\nTransform the content of the request with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmResponseTransformer }\n\n## Wasm Response Transformer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmResponseTransformer`\n\n### Description\n\nTransform the content of a response with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouteMatcher }\n\n## Wasm Route Matcher\n\n### Defined on steps\n\n - `MatchRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouteMatcher`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as route matcher\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouter }\n\n## Wasm Router\n\n### Defined on steps\n\n - `Router`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouter`\n\n### Description\n\nCan decide for routing with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmSink }\n\n## Wasm Sink\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmSink`\n\n### Description\n\nHandle unmatched requests with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XForwardedHeaders }\n\n## X-Forwarded-* headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XForwardedHeaders`\n\n### Description\n\nThis plugin adds all the X-Forwarded-* headers to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonRequest }\n\n## request body xml-to-json\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonRequest`\n\n### Description\n\nThis plugin transform incoming request body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonResponse }\n\n## response body xml-to-json\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonResponse`\n\n### Description\n\nThis plugin transform response body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ZipFileBackend }\n\n## Zip file backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ZipFileBackend`\n\n### Description\n\nServes content from a zip file\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://github.com/MAIF/otoroshi/releases/download/16.11.2/otoroshi-manual-16.11.2.zip\",\n \"headers\" : { },\n \"dir\" : \"./zips\",\n \"prefix\" : null,\n \"ttl\" : 3600000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.tunnel.TunnelPlugin }\n\n## Remote tunnel calls\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.tunnel.TunnelPlugin`\n\n### Description\n\nThis plugin can contact remote service using tunnels\n\n\n\n### Default configuration\n\n```json\n{\n \"tunnel_id\" : \"default\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.wasm.proxywasm.NgCorazaWAF }\n\n## Coraza WAF\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.wasm.proxywasm.NgCorazaWAF`\n\n### Description\n\nCoraza WAF plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"ref\" : \"none\"\n}\n```\n\n\n\n\n\n@@@\n\n"},{"name":"create-plugins.md","id":"/plugins/create-plugins.md","url":"/plugins/create-plugins.html","title":"Create plugins","content":"# Create plugins\n\n@@@ warning\nThis section is under rewrite. The following content is deprecated\n@@@\n\nWhen everything has failed and you absolutely need a feature in Otoroshi to make your use case work, there is a solution. Plugins is the feature in Otoroshi that allow you to code how Otoroshi should behave when receiving, validating and routing an http request. With request plugin, you can change request / response headers and request / response body the way you want, provide your own apikey, etc.\n\n## Plugin types\n\nthere are many plugin types explained @ref:[here](./plugins.md) \n\n## Code and signatures\n\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/requestsink.scala#L14-L19\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/routing.scala#L75-L78\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/accessvalidator.scala#L65-L85\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/script.scala#269-L540\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/eventlistener.scala#L27-L48\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L69-L164\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L108-L110\n\n\nfor more information about APIs you can use\n\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#package\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.mvc.Results\n* https://github.com/MAIF/otoroshi\n* https://doc.akka.io/docs/akka/2.5/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/scaladsl/Source.html\n\n## Plugin examples\n\n@ref:[A lot of plugins](./built-in-plugins.md) comes with otoroshi, you can find them on [github](https://github.com/MAIF/otoroshi/tree/master/otoroshi/app/plugins)\n\n## Writing a plugin from Otoroshi UI\n\nLog into Otoroshi and go to `Settings (cog icon) / Plugins`. Here you can create multiple request transformer scripts and associate it with service descriptor later.\n\n@@@ div { .centered-img }\n\n@@@\n\nwhen you write for instance a transformer in the Otoroshi UI, do the following\n\n```scala\nimport akka.stream.Materializer\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass MyTransformer extends RequestTransformer {\n\n val logger = Logger(\"my-transformer\")\n\n // implements the methods you want\n}\n\n// WARN: do not forget this line to provide a working instance of your transformer to Otoroshi\nnew MyTransformer()\n```\n\nYou can use the compile button to check if the script compiles, or code the transformer in your IDE (see next point).\n\nThen go to a service descriptor, scroll to the bottom of the page, and select your transformer in the list\n\n@@@ div { .centered-img }\n\n@@@\n\n## Providing a transformer from Java classpath\n\nYou can write your own transformer using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"request-transformer-example\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1.x.x\"\n )\n```\n\n@@@ warning\nyou MUST provide plugins that lies in the `otoroshi_plugins` package or in a sub-package of `otoroshi_plugins`. If you do not, your plugin will not be found by otoroshi. for example\n\n```scala\npackage otoroshi_plugins.com.my.company.myplugin\n```\n\nalso you don't have to instantiate your plugin at the end of the file like in the Otoroshi UI\n@@@\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/transformer.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in your service descriptor, you can chose your transformer in the list. If you want to do it from the API, you have to defined the transformerRef using `cp:` prefix like \n\n```json\n{\n \"transformerRef\": \"cp:otoroshi_plugins.my.class.package.MyTransformer\"\n}\n```\n\n## Getting custom configuration from the Otoroshi config. file\n\nLet say you need to provide custom configuration values for a script, then you can customize a configuration file of Otoroshi\n\n```hocon\ninclude \"application.conf\"\n\notoroshi {\n scripts {\n enabled = true\n }\n}\n\nmy-transformer {\n env = \"prod\"\n maxRequestBodySize = 2048\n maxResponseBodySize = 2048\n}\n```\n\nthen start Otoroshi like\n\n```sh\njava -Dconfig.file=/path/to/custom.conf -jar otoroshi.jar\n```\n\nthen, in your transformer, you can write something like \n\n```scala\npackage otoroshi_plugins.com.example.otoroshi\n\nimport akka.stream.Materializer\nimport akka.stream.scaladsl._\nimport akka.util.ByteString\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass BodyLengthLimiter extends RequestTransformer {\n\n override def def transformResponseWithCtx(ctx: TransformerResponseContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxResponseBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n\n override def transformRequestWithCtx(ctx: TransformerRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxRequestBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n}\n```\n\n## Using a library that is not embedded in Otoroshi\n\nJust use the `classpath` option when running Otoroshi\n\n```sh\njava -cp \"/path/to/library.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nBe carefull as your library can conflict with other libraries used by Otoroshi and affect its stability\n\n## Enabling plugins\n\nplugins can be enabled per service from the service settings page or globally from the danger zone in the plugins section.\n\n## Full example\n\na full external plugin example can be found @link:[here](https://github.com/mathieuancelin/otoroshi-wasmer-plugin)\n"},{"name":"index.md","id":"/plugins/index.md","url":"/plugins/index.html","title":"Otoroshi plugins","content":"# Otoroshi plugins\n\nIn this sections, you will find informations about Otoroshi plugins system\n\n* @ref:[Plugins system](./plugins.md)\n* @ref:[Create plugins](./create-plugins.md)\n* @ref:[Built in plugins](./built-in-plugins.md)\n* @ref:[Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@ index\n\n* [Plugins system](./plugins.md)\n* [Create plugins](./create-plugins.md)\n* [Built in plugins](./built-in-plugins.md)\n* [Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@"},{"name":"plugins.md","id":"/plugins/plugins.md","url":"/plugins/plugins.html","title":"Otoroshi plugins system","content":"# Otoroshi plugins system\n\nOtoroshi includes several extension points that allows you to create your own plugins and support stuff not supported by default.\n\n## Kind of available plugins\n\n@@@ div { .plugin-kind }\n###Request Sink\n\nUsed when no services are matched in Otoroshi. Can reply with any content.\n@@@\n\n@@@ div { .plugin-kind }\n###Pre routing\n\nUsed to extract values (like custom apikeys) and provide them to other plugins or Otoroshi engine\n@@@\n\n@@@ div { .plugin-kind }\n###Access Validator\n\nUsed to validate if a request can pass or not based on whatever you want\n@@@\n\n@@@ div { .plugin-kind }\n###Request Transformer\n\nUsed to transform request, responses and their body. Can be used to return arbitrary content\n@@@\n\n@@@ div { .plugin-kind }\n###Event listener\n\nAny plugin type can listen to Otoroshi internal events and react to thems\n@@@\n\n@@@ div { .plugin-kind }\n###Job\n\nTasks that can run automatically once, on be scheduled with a cron expression or every defined interval\n@@@\n\n@@@ div { .plugin-kind }\n###Exporter\n\nUsed to export events and Otoroshi alerts to an external source\n@@@\n\n@@@ div { .plugin-kind }\n###Request handler\n\nUsed to handle traffic without passing through Otoroshi routing and apply own rules\n@@@\n\n@@@ div { .plugin-kind }\n###Nano app\n\nUsed to write an api directly in Otoroshi in Scala language\n@@@"},{"name":"search.md","id":"/search.md","url":"/search.html","title":"Search otoroshi documentation","content":"# Search otoroshi documentation\n\n
      \n\n"},{"name":"anonymous-reporting.md","id":"/topics/anonymous-reporting.md","url":"/topics/anonymous-reporting.html","title":"Anonymous reporting","content":"# Anonymous reporting\n\nThe best way of supporting us in Otoroshi developement is to enable Anonymous reporting.\n\n## Details\n\nWhen this feature is active, Otoroshi perdiodically send anonymous information about its configuration.\n\nThis information helps us to know how Otoroshi is used, it's a precious hint to prioritise our roadmap.\n\nBelow is an example of what is send by Otoroshi. You can find more information about these fields either on @ref:[entities documentation](../entities/index.md) or [by reading the source code](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/jobs/reporting.scala#L174-L458).\n\n```json\n{\n \"@timestamp\": 1679514537259,\n \"timestamp_str\": \"2023-03-22T20:48:57.259+01:00\",\n \"@id\": \"4edb54171-8156-4947-b821-41d6c2bd1ba7\",\n \"otoroshi_cluster_id\": \"1148aee35-a487-47b0-b494-a2a44862c618\",\n \"otoroshi_version\": \"16.0.0-dev\",\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"datastore\": \"file\",\n \"env\": \"dev\",\n \"features\": {\n \"snow_monkey\": false,\n \"clever_cloud\": false,\n \"kubernetes\": false,\n \"elastic_read\": true,\n \"lets_encrypt\": false,\n \"auto_certs\": false,\n \"wasm_manager\": true,\n \"backoffice_login\": false\n },\n \"stats\": {\n \"calls\": 3823,\n \"data_in\": 480406,\n \"data_out\": 4698261,\n \"rate\": 0,\n \"duration\": 35.89899494949495,\n \"overhead\": 24.696984848484846,\n \"data_in_rate\": 0,\n \"data_out_rate\": 0,\n \"concurrent_requests\": 0\n },\n \"engine\": {\n \"uses_new\": true,\n \"uses_new_full\": false\n },\n \"cluster\": {\n \"mode\": \"Leader\",\n \"all_nodes\": 1,\n \"alive_nodes\": 1,\n \"leaders_count\": 1,\n \"workers_count\": 0,\n \"nodes\": [\n {\n \"id\": \"node_15ac62ec3-3e0d-48c1-a8ea-15de97088e3c\",\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"version\": \"16.0.0-dev\",\n \"type\": \"Leader\",\n \"cpu_usage\": 10.992902320605205,\n \"load_average\": 44.38720703125,\n \"heap_used\": 527,\n \"heap_size\": 2048,\n \"relay\": true,\n \"tunnels\": 0\n }\n ]\n },\n \"entities\": {\n \"scripts\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"routes\": {\n \"count\": 24,\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"router_routes\": {\n \"count\": 27,\n \"http_clients\": {\n \"ahc\": 25,\n \"akka\": 2,\n \"netty\": 0,\n \"akka_ws\": 0\n },\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"route_compositions\": {\n \"count\": 1,\n \"plugins\": {\n \"min\": 1,\n \"max\": 1,\n \"avg\": 1\n },\n \"by_kind\": {\n \"global\": 1\n }\n },\n \"apikeys\": {\n \"count\": 6,\n \"by_kind\": {\n \"disabled\": 0,\n \"with_rotation\": 0,\n \"with_read_only\": 0,\n \"with_client_id_only\": 0,\n \"with_constrained_services\": 0,\n \"with_meta\": 2,\n \"with_tags\": 1\n },\n \"authorized_on\": {\n \"min\": 1,\n \"max\": 4,\n \"avg\": 2\n }\n },\n \"jwt_verifiers\": {\n \"count\": 6,\n \"by_strategy\": {\n \"pass_through\": 6\n },\n \"by_alg\": {\n \"HSAlgoSettings\": 6\n }\n },\n \"certificates\": {\n \"count\": 9,\n \"by_kind\": {\n \"auto_renew\": 6,\n \"exposed\": 6,\n \"client\": 1,\n \"keypair\": 1\n }\n },\n \"auth_modules\": {\n \"count\": 8,\n \"by_kind\": {\n \"basic\": 7,\n \"oauth2\": 1\n }\n },\n \"service_descriptors\": {\n \"count\": 3,\n \"plugins\": {\n \"old\": 0,\n \"new\": 0\n },\n \"by_kind\": {\n \"disabled\": 1,\n \"fault_injection\": 0,\n \"health_check\": 1,\n \"gzip\": 0,\n \"jwt\": 0,\n \"cors\": 1,\n \"auth\": 0,\n \"protocol\": 0,\n \"restrictions\": 0\n }\n },\n \"teams\": {\n \"count\": 2\n },\n \"tenants\": {\n \"count\": 2\n },\n \"service_groups\": {\n \"count\": 2\n },\n \"data_exporters\": {\n \"count\": 10,\n \"by_kind\": {\n \"elastic\": 5,\n \"file\": 2,\n \"metrics\": 1,\n \"console\": 1,\n \"s3\": 1\n }\n },\n \"otoroshi_admins\": {\n \"count\": 5,\n \"by_kind\": {\n \"simple\": 2,\n \"webauthn\": 3\n }\n },\n \"backoffice_sessions\": {\n \"count\": 1,\n \"by_kind\": {\n \"simple\": 1\n }\n },\n \"private_apps_sessions\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"tcp_services\": {\n \"count\": 0\n }\n },\n \"plugins_usage\": {\n \"cp:otoroshi.next.plugins.AdditionalHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.DisableHttp10\": 2,\n \"cp:otoroshi.next.plugins.OverrideHost\": 27,\n \"cp:otoroshi.next.plugins.TailscaleFetchCertificate\": 1,\n \"cp:otoroshi.next.plugins.OtoroshiInfos\": 6,\n \"cp:otoroshi.next.plugins.MissingHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.Redirection\": 2,\n \"cp:otoroshi.next.plugins.OtoroshiChallenge\": 5,\n \"cp:otoroshi.next.plugins.BuildMode\": 2,\n \"cp:otoroshi.next.plugins.XForwardedHeaders\": 2,\n \"cp:otoroshi.next.plugins.NgLegacyAuthModuleCall\": 2,\n \"cp:otoroshi.next.plugins.Cors\": 4,\n \"cp:otoroshi.next.plugins.OtoroshiHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.NgDefaultRequestBody\": 1,\n \"cp:otoroshi.next.plugins.NgHttpClientCache\": 1,\n \"cp:otoroshi.next.plugins.ReadOnlyCalls\": 2,\n \"cp:otoroshi.next.plugins.RemoveHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.JwtVerificationOnly\": 1,\n \"cp:otoroshi.next.plugins.ApikeyCalls\": 3,\n \"cp:otoroshi.next.plugins.WasmAccessValidator\": 3,\n \"cp:otoroshi.next.plugins.WasmBackend\": 3,\n \"cp:otoroshi.next.plugins.IpAddressAllowedList\": 2,\n \"cp:otoroshi.next.plugins.AuthModule\": 4,\n \"cp:otoroshi.next.plugins.RemoveHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.IpAddressBlockList\": 2,\n \"cp:otoroshi.next.proxy.ProxyEngine\": 1,\n \"cp:otoroshi.next.plugins.JwtVerification\": 3,\n \"cp:otoroshi.next.plugins.GzipResponseCompressor\": 2,\n \"cp:otoroshi.next.plugins.SendOtoroshiHeadersBack\": 3,\n \"cp:otoroshi.next.plugins.AdditionalHeadersIn\": 4,\n \"cp:otoroshi.next.plugins.SOAPAction\": 1,\n \"cp:otoroshi.next.plugins.NgLegacyApikeyCall\": 6,\n \"cp:otoroshi.next.plugins.ForceHttpsTraffic\": 2,\n \"cp:otoroshi.next.plugins.NgErrorRewriter\": 1,\n \"cp:otoroshi.next.plugins.MissingHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.MaintenanceMode\": 3,\n \"cp:otoroshi.next.plugins.RoutingRestrictions\": 2,\n \"cp:otoroshi.next.plugins.HeadersValidation\": 2\n }\n}\n```\n\n## Toggling\n\nAnonymous reporting can be toggled any time using :\n\n- the UI (Features > Danger zone > Send anonymous reports)\n- `otoroshi.anonymous-reporting.enabled` configuration\n- `OTOROSHI_ANONYMOUS_REPORTING_ENABLED` env variable\n"},{"name":"chaos-engineering.md","id":"/topics/chaos-engineering.md","url":"/topics/chaos-engineering.html","title":"Chaos engineering with the Snow Monkey","content":"# Chaos engineering with the Snow Monkey\n\nNihonzaru (the Snow Monkey) is the chaos engineering tool provided by Otoroshi. You can access it at `Settings (cog icon) / Snow Monkey`.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Chaos engineering\n\nOtoroshi offers some tools to introduce [chaos engineering](https://principlesofchaos.org/) in your everyday life. With chaos engineering, you will improve the resilience of your architecture by creating faults in production on running systems. With [Nihonzaru (the snow monkey)](https://en.wikipedia.org/wiki/Japanese_macaque) Otoroshi helps you to create faults on http request/response handled by Otoroshi. \n\n@@@ div { .centered-img }\n\n@@@\n\n## Settings\n\n@@@ div { .centered-img }\n\n@@@\n\nThe snow monkey let you define a few settings to work properly :\n\n* **Include user facing apps.**: you want to create fault in production, but maybe you don't want your users to enjoy some nice snow monkey generated error pages. Using this switch let you include of not user facing apps (ui apps). Each service descriptor has a `User facing app switch` that will be used by the snow monkey.\n* **Dry run**: when dry run is enabled, outages will be registered and will generate events and alerts (in the otoroshi eventing system) but requests won't be actualy impacted. It's a good way to prepare applications to the snow monkey habits\n* **Outage strategy**: Either `AllServicesPerGroup` or `OneServicePerGroup`. It means that only one service per group or all services per groups will have `n` outages (see next bullet point) during the snow monkey working period\n* **Outages per day**: during snow monkey working period, each service per group or one service per group will have only `n` outages registered \n* **Working period**: the snow monkey only works during a working period. Here you can defined when it starts and when it stops\n* **Outage duration**: here you can defined the bounds for the random outage duration when an outage is created on a service\n* **Impacted groups**: here you can define a list of service groups impacted by the snow monkey. If none is specified, then all service groups will be impacted\n\n## Faults\n\nWith the snow monkey, you can generate four types of faults\n\n* **Large request fault**: Add trailing bytes at the end of the request body (if one)\n* **Large response fault**: Add trailing bytes at the end of the response body\n* **Latency injection fault**: Add random response latency between two bounds\n* **Bad response injection fault**: Create predefined responses with custom headers, body and status code\n\nEach fault let you define a ratio for impacted requests. If you specify a ratio of `0.2`, then 20% of the requests for the impacte service will be impacted by this fault\n\n@@@ div { .centered-img }\n\n@@@\n\nThen you juste have to start the snow monkey and enjoy the show ;)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Current outages\n\nIn the last section of the snow monkey page, you can see current outages (per service), when they started, their duration, etc ...\n\n@@@ div { .centered-img }\n\n@@@"},{"name":"dev-portal.md","id":"/topics/dev-portal.md","url":"/topics/dev-portal.html","title":"Developer portal with Daikoku","content":"# Developer portal with Daikoku\n\nWhile Otoroshi is the perfect tool to manage your webapps in a technical point of view it lacked of business perspective. This is not the case anymore with Daikoku.\n\nWhile Otoroshi is a standalone, Daikoku is a developer portal which stands in front of Otoroshi and provides some business feature.\n\nWhether you want to use Daikoku for your public APIs, you want to monetize or with your private APIs to provide some documentation, facilitation and self-service feature, it will be the perfect portal for Otoroshi.\n\n@@@div { .plugin .platform }\n## Daikoku\n\nRun your first Daikoku with a simple jar or with one Docker command.\n\n\n
      \nTry Daikoku \n
      \n@link:[With jar](https://maif.github.io/daikoku/devmanual/getdaikoku/frombinaries.html)\n@link:[With Docker](https://maif.github.io/daikoku/devmanual/getdaikoku/fromdocker.html)\n@@@\n\n@@@div { .plugin .platform }\n## Contribute\n\nDaikoku is opensource, so all contributions are welcome.\n\n\n@link:[Show the repository](https://github.com/MAIF/daikoku)\n@@@\n\n@@@div { .plugin .platform }\n## Documentation\n\nDaikoku and its UI are fully documented.\n\n\n@link:[Read the documentation](https://maif.github.io/daikoku/devmanual/)\n@@@\n\n"},{"name":"engine.md","id":"/topics/engine.md","url":"/topics/engine.html","title":"Proxy engine","content":"# Proxy engine\n\nStarting from the `1.5.3` release, otoroshi offers a new plugin that implements the next generation of the proxy engine. \nThis engine has been designed based on our 5 years experience building, maintaining and running the previous one.\nIt tries to fix all the drawback we may have encountered during those years and highly improve performances, user experience, reporting and debugging capabilities. \n\nThe new engine is fully plugin oriented in order to spend CPU cycles only on useful stuff. You can enable this plugin only on some domain names so you can easily A/B test the new engine. The new proxy engine is designed to be more reactive and more efficient generally. It is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nStarting from version `16.0.0`, this engine will be enabled by default on any new otoroshi cluster. In a future version, the engine will be enabled for any new or exisiting otoroshi cluster.\n\n## Enabling the new engine\n\nBy default, all freshly started Otoroshi instances have the new proxy engine enabled by default, for the other, to enable the new proxy engine on an otoroshi instance, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration, enable it and in `domains` add the values of the desired domains (let say we want to use the new engine on `api.foo.bar`. It is possible to use `*.foo.bar` if that's what you want to do).\n\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the previous one.\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"debug_headers\" : false,\n \"reporting\": true,\n \"domains\" : [ \"api.foo.bar\" ],\n \"deny_domains\" : [ ],\n }\n}\n```\n\nif you need to enable global plugin with the new engine, you can add the following configuration in the `global plugins` configuration object \n\n```javascript\n{\n ...\n \"ng\": {\n \"slots\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.W3CTracing\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"baggage\": {\n \"foo\": \"bar\"\n }\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.RequestSinkWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.apikeys.ClientCredentialService\",\n \"ClientCredentialService\": {\n \"domain\": \"ccs-next-gen.oto.tools\",\n \"expiration\": 3600000,\n \"defaultKeyPair\": \"otoroshi-jwt-signing\",\n \"secure\": false\n }\n }\n }\n ]\n }\n ...\n}\n```\n\n## Entities\n\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `backends`: a list of targets to contact a backend\n\n## Entities sync\n\nA new behavior introduced for the new proxy engine is the entities sync job. To avoid unecessary operations on the underlying datastore when routing requests, a new job has been setup in otoroshi that synchronize the content of the datastore (at least a part of it) with an in-memory cache. Because of it, the propagation of changes between an admin api call and the actual result on routing can be longer than before. When a node creates, updates, or deletes an entity via the admin api, other nodes need to wait for the next poll to purge the old cached entity and start using the new one. You can change the interval between syncs with the configuration key `otoroshi.next.state-sync-interval` or the env. variable `OTOROSHI_NEXT_STATE_SYNC_INTERVAL`. The default value is `10000` and the unit is `milliseconds`\n\n@@@ warning\nBecause of entities sync, memory consumption of otoroshi will be significantly higher than previous versions. You can use `otoroshi.next.monitor-proxy-state-size=true` config (or `OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE` env. variable) to monitor the actual memory size of the entities cache. This will produce the `ng-proxy-state-size-monitoring` metric in standard otoroshi metrics\n@@@\n\n## Automatic conversion\n\nThe new engine uses new entities for its configuration, but in order to facilitate transition between the old world and the new world, all the `service descriptors` of an otoroshi instance are automatically converted live into `routes` periodically. Any `service descriptor` should still work as expected through the new engine while enjoying all the perks.\n\n@@@ warning\nthe experimental nature of the engine can imply unexpected behaviors for converted service descriptors\n@@@\n\n## Routing\n\nthe new proxy engine introduces a new router that has enhanced capabilities and performances. The router can handle thousands of routes declarations without compromising performances.\n\nThe new route allow routes to be matched on a combination of\n\n* hostname\n* path\n* header values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n* query param values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n\npatch matching works \n\n* exactly\n * matches `/api/foo` with `/api/foo` and not with `/api/foo/bar`\n* starting with value (default behavior, like the previous engine)\n * matches `/api/foo` with `/api/foo` but also with `/api/foo/bar`\n\npath matching can also include wildcard paths and even path params\n\n* plain old path: `subdomain.domain.tld/api/users`\n* wildcard path: `subdomain.domain.tld/api/users/*/bills`\n* named path params: `subdomain.domain.tld/api/users/:id/bills`\n* named regex path params: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n\nhostname matching works on \n\n* exact values\n * `subdomain.domain.tld`\n* wildcard values like\n * `*.domain.tld`\n * `subdomain.*.tld`\n\nas path matching can now include named path params, it is possible to perform a ful url rewrite on the target path like \n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Plugins\n\nthe new route entity defines a plugin pipline where any plugin can be enabled or not and can be active only on some paths. \nEach plugin slot in the pipeline holds the plugin id and the plugin configuration. \n\nYou can also enable debugging only on a plugin instance instead of the whole route (see [the debugging section](#debugging))\n\n```javascript\n{ \n ...\n \"plugins\" : [ {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.OverrideHost\",\n \"include\" : [ ],\n \"exclude\" : [ ],\n \"config\" : { }\n }, {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\" : [ ],\n \"exclude\" : [ \"/openapi.json\" ],\n \"config\" : { }\n } ]\n}\n```\n\nyou can find the list of built-in plugins @ref:[here](../plugins/built-in-plugins.md)\n\n## Using legacy plugins\n\nif you need to use legacy otoroshi plugins with the new engine, you can use several wrappers in order to do so\n\n* `otoroshi.next.plugins.wrappers.PreRoutingWrapper`\n* `otoroshi.next.plugins.wrappers.AccessValidatorWrapper`\n* `otoroshi.next.plugins.wrappers.RequestSinkWrapper`\n* `otoroshi.next.plugins.wrappers.RequestTransformerWrapper`\n* `otoroshi.next.plugins.wrappers.CompositeWrapper`\n\nto use it, just declare a plugin slot with the right wrapper and in the config, declare the `plugin` you want to use and its configuration like:\n\n```javascript\n{\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.PreRoutingWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.jwt.JwtUserExtractor\",\n \"JwtUserExtractor\": {\n \"verifier\" : \"$ref\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\": \"email\",\n \"metaPath\" : null\n }\n }\n}\n```\n\n## Reporting\n\nby default, any request hiting the new engine will generate an execution report with informations about how the request pipeline steps were performed. It is possible to export those reports as `RequestFlowReport` events using classical data exporter. By default, exporting for reports is not enabled, you must enable the `export_reporting` flag on a `route` or `service`.\n\n```javascript\n{\n \"@id\": \"8efac472-07bc-4a80-8d27-4236309d7d01\",\n \"@timestamp\": \"2022-02-15T09:51:25.402+01:00\",\n \"@type\": \"RequestFlowReport\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"service_548f13bb-a809-4b1d-9008-fae3b1851092\",\n \"@service\": \"demo-service\",\n \"@env\": \"prod\",\n \"route\": {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"report\": {\n \"id\" : \"ab73707b3-946b-4853-92d4-4c38bbaac6d6\",\n \"creation\" : \"2022-02-15T09:51:25.402+01:00\",\n \"termination\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 5,\n \"duration_ns\" : 5905522,\n \"overhead\" : 4,\n \"overhead_ns\" : 4223215,\n \"overhead_in\" : 2,\n \"overhead_in_ns\" : 2687750,\n \"overhead_out\" : 1,\n \"overhead_out_ns\" : 1535465,\n \"state\" : \"Successful\",\n \"steps\" : [ {\n \"task\" : \"start-handling\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 177430,\n \"ctx\" : null\n }, {\n \"task\" : \"check-concurrent-requests\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 145242,\n \"ctx\" : null\n }, {\n \"task\" : \"find-route\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 497119,\n \"ctx\" : {\n \"found_route\" : {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"matched_path\" : \"\",\n \"exact\" : true,\n \"params\" : { },\n \"matched_routes\" : [ \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\" ]\n }\n }, {\n \"task\" : \"compute-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 105151,\n \"ctx\" : {\n \"disabled_plugins\" : [ ],\n \"filtered_plugins\" : [ ]\n }\n }, {\n \"task\" : \"tenant-check\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 26097,\n \"ctx\" : null\n }, {\n \"task\" : \"check-global-maintenance\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 14132,\n \"ctx\" : null\n }, {\n \"task\" : \"call-before-request-callbacks\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 56671,\n \"ctx\" : null\n }, {\n \"task\" : \"extract-tracking-id\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 5207,\n \"ctx\" : null\n }, {\n \"task\" : \"call-pre-route-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 39786,\n \"ctx\" : null\n }, {\n \"task\" : \"call-access-validator-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 25311,\n \"ctx\" : null\n }, {\n \"task\" : \"enforce-global-limits\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 296617,\n \"ctx\" : {\n \"remaining_quotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 10000000,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 10000000,\n \"remainingCallsPerDay\" : 10000000,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 10000000,\n \"remainingCallsPerMonth\" : 10000000\n }\n }\n }, {\n \"task\" : \"choose-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 368899,\n \"ctx\" : {\n \"backend\" : {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n }\n }, {\n \"task\" : \"transform-request\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 506363,\n \"ctx\" : null\n }, {\n \"task\" : \"call-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 2,\n \"duration_ns\" : 2163470,\n \"ctx\" : null\n }, {\n \"task\" : \"transform-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 279887,\n \"ctx\" : null\n }, {\n \"task\" : \"stream-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 382952,\n \"ctx\" : null\n }, {\n \"task\" : \"trigger-analytics\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 812036,\n \"ctx\" : null\n }, {\n \"task\" : \"request-success\",\n \"start\" : 1644915085408,\n \"start_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 0,\n \"ctx\" : null\n } ]\n }\n}\n```\n\n## Debugging\n\nwith the new reporting capabilities, the new engine also have debugging capabilities built in. In you enable the `debug_flow` flag on a route (or service), the resulting `RequestFlowReport` will be enriched with contextual informations between each plugins of the route plugin pipeline\n\n@@@ note\nyou can also use the `Try it` feature of the new route designer UI to get debug reports automatically for a specific call\n@@@\n\n## HTTP traffic capture\n\nusing the `capture` flag, a `TrafficCaptureEvent` is generated for each http request/response. This event will contains request and response body. Those events can be exported using @ref:[data exporters](../entities/data-exporters.md) as usual. You can also use the @ref:[GoReplay file exporter](../entities/data-exporters.md#goreplay-file) that is specifically designed to ingest those events and create [GoReplay](https://goreplay.org/) files (`.gor`)\n\n@@@ warning\nthis feature can have actual impact on CPU and RAM consumption\n@@@\n\n```json\n{\n \"@id\": \"d5998b0c4-cb08-43e6-9921-27472c7a56e0\",\n \"@timestamp\": 1651828801115,\n \"@type\": \"TrafficCaptureEvent\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"@service\": \"test-server\",\n \"@env\": \"prod\",\n \"route\": {\n \"id\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"name\": \"test-server\"\n },\n \"request\": {\n \"id\": \"152250645825034725600000\",\n \"int_id\": 115,\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"test-server-next-gen.oto.tools:9999\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\",\n \"Remote-Address\": \"127.0.0.1:57660\",\n \"Timeout-Access\": \"\",\n \"Raw-Request-URI\": \"/\",\n \"Tls-Session-Info\": \"Session(1651828041285|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"path\": \"/\",\n \"domain\": null,\n \"http_only\": true,\n \"max_age\": null,\n \"secure\": false,\n \"same_site\": null\n }\n ],\n \"tls\": false,\n \"uri\": \"/\",\n \"path\": \"/\",\n \"version\": \"HTTP/1.1\",\n \"has_body\": true,\n \"remote\": \"127.0.0.1\",\n \"client_cert_chain\": null,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_request\": {\n \"url\": \"http://localhost:3000/\",\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"localhost\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\"\n },\n \"version\": \"HTTP/1.1\",\n \"client_cert_chain\": null,\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"domain\": null,\n \"path\": \"/\",\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": true\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"int_id\": 33,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_response\": {\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"response\": {\n \"id\": \"152250645825034725600000\",\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"user-agent-details\": null,\n \"origin-details\": null,\n \"instance-number\": 0,\n \"instance-name\": \"dev\",\n \"instance-zone\": \"local\",\n \"instance-region\": \"local\",\n \"instance-dc\": \"local\",\n \"instance-provider\": \"local\",\n \"instance-rack\": \"local\",\n \"cluster-mode\": \"Leader\",\n \"cluster-name\": \"otoroshi-leader-9hnv5HUXpbCZD7Ee\"\n}\n```\n\n## openapi import\n\nas the new router offers possibility to match exactly on a single path and a single method, and with the help of the `service` entity, it is now pretty easy to import openapi document as `route-compositions` entities. To do that, a new api has been made available to perform the translation. Be aware that this api **DOES NOT** save the entity and just return the result of the translation. \n\n```sh\ncurl -X POST \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/route-compositions/_openapi' \\\n -d '{\"domain\":\"oto-api-proxy.oto.tools\",\"openapi\":\"https://raw.githubusercontent.com/MAIF/otoroshi/master/otoroshi/public/openapi.json\"}'\n```\n\n@@@ div { .centered-img }\n\n@@@\n\n"},{"name":"events-and-analytics.md","id":"/topics/events-and-analytics.md","url":"/topics/events-and-analytics.html","title":"Events and analytics","content":"# Events and analytics\n\nOtoroshi is a solution fully traced : calls to services, access to UI, creation of resources, etc.\n\n@@@ warning\nYou have to use [Elastic](https://www.elastic.co) to enable analytics features in Otoroshi\n@@@\n\n## Events\n\n* Analytics event\n* Gateway event\n* TCP event\n* Healthcheck event\n\n## Event log\n\nOtoroshi can read his own exported events from an Elasticsearch instance, set up in the danger zone. Theses events are available from the UI, at the following route: `https://xxxxx/bo/dashboard/events`.\n\nThe `Global events` page display all events of **GatewayEvent** type. This page is a way to quickly read an interval of events and can be used in addition of a Kibana instance.\n\nFor each event, a list of information will be displayed and an additional button `content` to watch the full content of the event, at the JSON format. \n\n## Alerts \n\n* `MaxConcurrentRequestReachedAlert`: happening when the handled requests number are greater than the limit of concurrent requests indicated in the global configuration of Otoroshi\n* `CircuitBreakerOpenedAlert`: happening when the circuit breaker pass from closed to opened\n* `CircuitBreakerClosedAlert`: happening when the circuit breaker pass from opened to closed\n* `SessionDiscardedAlert`: send when an admin discarded an admin sessions\n* `SessionsDiscardedAlert`: send when an admin discarded all admin sessions\n* `PanicModeAlert`: send when panic mode is enabled\n* `OtoroshiExportAlert`: send when otoroshi global configuration is exported\n* `U2FAdminDeletedAlert`: send when an admin has deleted an other admin user\n* `BlackListedBackOfficeUserAlert`: send when a blacklisted user has tried to acccess to the UI\n* `AdminLoggedInAlert`: send when an user admin has logged to the UI\n* `AdminFirstLogin`: send when an user admin has successfully logged to the UI for the first time\n* `AdminLoggedOutAlert`: send when an user admin has logged out from Otoroshi\n* `GlobalConfigModification`: send when an user amdin has changed the global configuration of Otoroshi\n* `RevokedApiKeyUsageAlert`: send when an user admin has revoked an apikey\n* `ServiceGroupCreatedAlert`: send when an user admin has created a service group\n* `ServiceGroupUpdatedAlert`: send when an user admin has updated a service group\n* `ServiceGroupDeletedAlert`: send when an user admin has deleted a service group\n* `ServiceCreatedAlert`: send when an user admin has created a tcp service\n* `ServiceUpdatedAlert`: send when an user admin has updated a tcp service\n* `ServiceDeletedAlert`: send when an user admin has deleted a tcp service\n* `ApiKeyCreatedAlert`: send when an user admin has crated a new apikey\n* `ApiKeyUpdatedAlert`: send when an user admin has updated a new apikey\n* `ApiKeyDeletedAlert`: send when an user admin has deleted a new apikey\n\n## Audit\n\nWith Otoroshi, any admin action and any sucpicious/alert action is recorded. These records are stored in Otoroshi’s datastore (only the last n records, defined by the `otoroshi.events.maxSize` config key). All the records can be send through the analytics mechanism (WebHook, Kafka, Elastic) for external and/or further usage. We recommand sending away those records for security reasons.\n\nOtoroshi keep the following list of information for each executed action:\n\n* `Date`: moment of the action\n* `User`: name of the owner\n* `From`: IP of the concerned user\n* `Action`: action performed by the person. The possible actions are:\n\n * `ACCESS_APIKEY`: User accessed a apikey\n * `ACCESS_ALL_APIKEYS`: User accessed all apikeys\n * `CREATE_APIKEY`: User created a apikey\n * `UPDATE_APIKEY`: User updated a apikey\n * `DELETE_APIKEY`: User deleted a apikey\n * `ACCESS_AUTH_MODULE`: User accessed an Auth. module\n * `ACCESS_ALL_AUTH_MODULES`: User accessed all Auth. modules\n * `CREATE_AUTH_MODULE`: User created an Auth. module\n * `UPDATE_AUTH_MODULE`: User updated an Auth. module\n * `DELETE_AUTH_MODULE`: User deleted an Auth. module\n * `ACCESS_CERTIFICATE`: User accessed a certificate\n * `ACCESS_ALL_CERTIFICATES`: User accessed all certificates\n * `CREATE_CERTIFICATE`: User created a certificate\n * `UPDATE_CERTIFICATE`: User updated a certificate\n * `DELETE_CERTIFICATE`: User deleted a certificate\n * `ACCESS_CLIENT_CERT_VALIDATOR`: User accessed a client cert. validator\n * `ACCESS_ALL_CLIENT_CERT_VALIDATORS`: User accessed all client cert. validators\n * `CREATE_CLIENT_CERT_VALIDATOR`: User created a client cert. validator\n * `UPDATE_CLIENT_CERT_VALIDATOR`: User updated a client cert. validator\n * `DELETE_CLIENT_CERT_VALIDATOR`: User deleted a client cert. validator\n * `ACCESS_DATA_EXPORTER_CONFIG`: User accessed a data exporter config\n * `ACCESS_ALL_DATA_EXPORTER_CONFIG`: User accessed all data exporter config\n * `CREATE_DATA_EXPORTER_CONFIG`: User created a data exporter config\n * `UPDATE_DATA_EXPORTER_CONFIG`: User updated a data exporter config\n * `DELETE_DATA_EXPORTER_CONFIG`: User deleted a data exporter config\n * `ACCESS_GLOBAL_JWT_VERIFIER`: User accessed a global jwt verifier\n * `ACCESS_ALL_GLOBAL_JWT_VERIFIERS`: User accessed all global jwt verifiers\n * `CREATE_GLOBAL_JWT_VERIFIER`: User created a global jwt verifier\n * `UPDATE_GLOBAL_JWT_VERIFIER`: User updated a global jwt verifier\n * `DELETE_GLOBAL_JWT_VERIFIER`: User deleted a global jwt verifier\n * `ACCESS_SCRIPT`: User accessed a script\n * `ACCESS_ALL_SCRIPTS`: User accessed all scripts\n * `CREATE_SCRIPT`: User created a script\n * `UPDATE_SCRIPT`: User updated a script\n * `DELETE_SCRIPT`: User deleted a Script\n * `ACCESS_SERVICES_GROUP`: User accessed a service group\n * `ACCESS_ALL_SERVICES_GROUPS`: User accessed all services groups\n * `CREATE_SERVICE_GROUP`: User created a service group\n * `UPDATE_SERVICE_GROUP`: User updated a service group\n * `DELETE_SERVICE_GROUP`: User deleted a service group\n * `ACCESS_SERVICES_FROM_SERVICES_GROUP`: User accessed all services from a services group\n * `ACCESS_TCP_SERVICE`: User accessed a tcp service\n * `ACCESS_ALL_TCP_SERVICES`: User accessed all tcp services\n * `CREATE_TCP_SERVICE`: User created a tcp service\n * `UPDATE_TCP_SERVICE`: User updated a tcp service\n * `DELETE_TCP_SERVICE`: User deleted a tcp service\n * `ACCESS_TEAM`: User accessed a Team\n * `ACCESS_ALL_TEAMS`: User accessed all teams\n * `CREATE_TEAM`: User created a team\n * `UPDATE_TEAM`: User updated a team\n * `DELETE_TEAM`: User deleted a team\n * `ACCESS_TENANT`: User accessed a Tenant\n * `ACCESS_ALL_TENANTS`: User accessed all tenants\n * `CREATE_TENANT`: User created a tenant\n * `UPDATE_TENANT`: User updated a tenant\n * `DELETE_TENANT`: User deleted a tenant\n * `SERVICESEARCH`: User searched for a service\n * `ACTIVATE_PANIC_MODE`: Admin activated panic mode\n\n\n* `Message`: explicit message about the action (example: the `SERVICESEARCH` action happened when an `user searched for a service`)\n* `Content`: all information at JSON format\n\n## Global metrics\n\nThe global metrics are displayed on the index page of the Otoroshi UI. Otoroshi provides information about :\n\n* the number of requests served\n* the amount of data received and sended\n* the number of concurrent requests\n* the number of requests per second\n* the current overhead\n\nMore metrics can be found on the **Global analytics** page (available at https://xxxxxx/bo/dashboard/stats).\n\n## Monitoring services\n\nOnce you have declared services, you can monitor them with Otoroshi. \n\nLet's starting by setup Otoroshi to push events to an elastic cluster via a data exporter. Then you will can setup Otoroshi events read from an elastic cluster. Go to `settings (cog icon) / Danger Zone` and expand the `Analytics: Elastic cluster (read)` section.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service healthcheck\n\nIf you have defined an health check URL in the service descriptor, you can access the health check page from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service live stats\n\nYou can also monitor live stats like total of served request, average response time, average overhead, etc. The live stats page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service analytics\n\nYou can also get some aggregated metrics. The analytics page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n## New proxy engine\n\n### Debug reporting\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `debug_flow` flag, events of type `RequestFlowReport` are generated\n\n### Traffic capture\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `capture` flag, events of type `TrafficCaptureEvent` are generated. It contains everything that compose otoroshi input http request and output http responses\n"},{"name":"expression-language.md","id":"/topics/expression-language.md","url":"/topics/expression-language.html","title":"Expression language","content":"# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.14.0/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```"},{"name":"graphql-composer.md","id":"/topics/graphql-composer.md","url":"/topics/graphql-composer.html","title":"GraphQL Composer Plugin","content":"# GraphQL Composer Plugin\n\n
      \nRoute plugins:\nGraphQL Composer\n
      \n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\n> GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.\n[Official GraphQL website](https://graphql.org/)\n\nAPIs RESTful and GraphQL development has become one of the most popular activities for companies as well as users in recent times. In fast scaling companies, the multiplication of clients can cause the number of API needs to grow at scale.\n\nOtoroshi comes with a solution to create and meet your customers' needs without constantly creating and recreating APIs: the `GraphQL composer plugin`. The GraphQL Composer is an useful plugin to build an GraphQL API from multiples differents sources. These sources can be REST apis, GraphQL api or anything that supports the HTTP protocol. In fact, the plugin can define and expose for each of your client a specific GraphQL schema, which only corresponds to the needs of the customers.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n## Tutorial\n\nLet's take an example to get a better view of this plugin. We want to build a schema with two types: \n\n* an user with a name and a password \n* an country with a name and its users.\n\nTo build this schema, we need to use three custom directives. A `directive` decorates part of a GraphQL schema or operation with additional configuration. Directives are preceded by the @ character, like so:\n\n* @ref:[rest](#directives) : to call a http rest service with dynamic path params\n* @ref:[permission](#directives) : to restrict the access to the sensitive field\n* @ref:[graphql](#directives) : to call a graphQL service by passing a url and the associated query\n\nThe final schema of our tutorial should look like this\n```graphql\ntype Country {\n name: String\n users: [User] @rest(url: \"http://localhost:5000/countries/${item.name}/users\")\n}\n\ntype User {\n name: String\n password: String @password(value: \"ADMIN\")\n}\n\ntype Query {\n users: [User] @rest(url: \"http://localhost:5000/users\", paginate: true)\n user(id: String): User @rest(url: \"http://localhost:5000/users/${params.id}\")\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com\", query: \"{ countries { name }}\", paginate: true)\n}\n```\n\nNow you know the GraphQL Composer basics and how it works, let's configure it on our project:\n\n* create a route using the new Otoroshi router describing the previous countries API\n* add the GraphQL composer plugin\n* configure the plugin with the schema\n* try to call it\n\n@@@ div { .centered-img }\n\n@@@\n\n### Setup environment\n\nFirst of all, we need to download the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v1.5.15/otoroshi.jar'\n```\n\nNow, just run the command belows to start the Otoroshi, and look the console to see the output.\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow, login to [the UI](http://otoroshi.oto.tools:8080) with \n```sh\nuser = admin@otoroshi.io\npassword = password\n```\n\n### Create our countries API\n\nFirst thing to do in any new API is of course creating a `route`. We need 4 informations which are:\n\n* name: `My countries API`\n* frontend: exposed on `countries-api.oto.tools`\n* plugins: the list of plugins with only the `GraphQL composer` plugin\n\nLet's make a request call through the Otoroshi Admin API (with the default apikey), like the example below\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\"countries.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\"\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\n### Build the countries API \n\nLet's continue our API by patching the configuration of the GraphQL plugin with the complete schema.\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/api/routes/countries-api' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\n \"countries.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\",\n \"config\": {\n \"schema\": \"type Country {\\n name: String\\n users: [User] @rest(url: \\\"http://localhost:8181/countries/${item.name}/users\\\", headers: \\\"{}\\\")\\n}\\n\\ntype Query {\\n users: [User] @rest(url: \\\"http://localhost:8181/users\\\", paginate: true, headers: \\\"{}\\\")\\n user(id: String): User @rest(url: \\\"http://localhost:8181/users/${params.id}\\\")\\n countries: [Country] @graphql(url: \\\"https://countries.trevorblades.com\\\", query: \\\"{ countries { name }}\\\", paginate: true)\\ntype User {\\n name: String\\n password: String }\\n\"\n }\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThe route is created but it expects an API, exposed on the localhost:8181, to work. \n\nLet's create this simple API which returns a list of users and of countries. This should look like the following snippet.\nThe API uses express as http server.\n\n```js\nconst express = require('express')\n\nconst app = express()\n\nconst users = [\n {\n name: 'Joe',\n password: 'password'\n },\n {\n name: 'John',\n password: 'password2'\n }\n]\n\nconst countries = [\n {\n name: 'Andorra',\n users: [users[0]]\n },\n {\n name: 'United Arab Emirates',\n users: [users[1]]\n }\n]\n\napp.get('/users', (_, res) => {\n return res.json(users)\n})\n\napp.get(`/users/:name`, (req, res) => {\n res.json(users.find(u => u.name === req.params.name))\n})\n\napp.get('/countries/:id/users', (req, res) => {\n const country = countries.find(c => c.name === req.params.id)\n\n if (country) \n return res.json(country.users)\n else \n return res.json([])\n})\n\napp.listen(8181, () => {\n console.log(`Listening on 8181`)\n});\n\n```\n\nLet's try to make a first call to our countries API.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": [\n { \n \"name\":\"Andorra\",\n \"users\": [\n { \"name\":\"Joe\" }\n ]\n }\n ]\n }\n}\n```\n\nThe call graph should looks like\n\n```\n1. Calls https://countries.trevorblades.com\n2. For each country:\n - extract the field name\n - calls http://localhost:8181/countries/${country}/users to get the list of users for this country\n```\n\nYou may have noticed that we added an argument at the end of the graphql directive named `paginate`. It enabled the paging for the client accepting limit and offset parameters. These parameters are used by the plugin to filter and reduce the content.\n\nLet's make a new call that does not accept any country.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": []\n }\n}\n```\n\nLet's move on to the next section to secure sensitive field of our API.\n\n### Basics of permissions \n\nThe permission directives has been created to protect the fields of the graphql schema. The validation process starts by create a `context` for all incoming requests, based on the list of paths defined in the permissions field of the plugin. The permissions paths can refer to the request data (url, headers, etc), user credentials (api key, etc) and informations about the matched route. Then the process can validate that the value or values are present in the `context`.\n\n@@@div { .simple-block }\n\n
      \nPermission\n\n
      \n\n*Arguments : value and unauthorized_value*\n\nThe permission directive can be used to secure a field on **one** value. The directive checks that a specific value is present in the `context`.\n\nTwo arguments are available, the first, named `value`, is required and designates the value found. The second optional value, `unauthorized_value`, can be used to indicates, in the outcoming response, the rejection message.\n\n**Example**\n```js\ntype User {\n id: String @permission(\n value: \"FOO\", \n unauthorized_value: \"You're not authorized to get this field\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nAll permissions\n\n
      \n\n*Arguments : values and unauthorized_value*\n\nThis directive is presumably the same as the previous one except that it takes a list of values.\n\n**Example**\n```js\ntype User {\n id: String @allpermissions(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO and BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nOne permissions of\n\n
      \n*Arguments : values and unauthorized_value*\n\nThis directive takes a list of values and validate that one of them is in the context.\n\n**Example**\n```js\ntype User {\n id: String @onePermissionsOf(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO or BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nAuthorize\n\n
      \n\n*Arguments : path, value and unauthorized_value*\n\nThe authorize directive has one more required argument, named `path`, which indicates the path to value, in the context. Unlike the last three directives, the authorize directive doesn't search in the entire context but at the specified path.\n\n**Example**\n```js\ntype User {\n id: String @authorize(\n path: \"$.raw_request.headers.foo\", \n value: \"BAR\", \n unauthorized_value: \"Bar could not be found in the foo header\")\n}\n```\n@@@\n\nLet's restrict the password field to the users that comes with a `role` header of the value `ADMIN`.\n\n1. Patch the configuration of the API by adding the permissions in the configuration of the plugin.\n```json\n...\n \"permissions\": [\"$.raw_request.headers.role\"]\n...\n```\n\n1. Add an directive on the password field in the schema\n```graphql\ntype User {\n name: String\n password: String @permission(value: \"ADMIN\")\n}\n```\n\nLet's make a call with the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: ADMIN'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nNow try to change the value of the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: USER'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nThe error message should look like \n\n```json\n{\n \"errors\": [\n {\n \"message\": \"You're not authorized\",\n \"path\": [\n \"countries\",\n 0,\n \"users\",\n 0,\n \"password\"\n ],\n ...\n }\n ]\n}\n```\n\n\n# Glossary\n\n## Directives\n\n@@@div { .simple-block }\n\n
      \nRest\n\n
      \n\n*Arguments : url, method, headers, timeout, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to expose servers that communicate using the http protocol. The only required argument is the `url`.\n\n**Example**\n```js\ntype Query {\n users(limit: Int, offset: Int): [User] @rest(url: \"http://foo.oto.tools/users\", method: \"GET\")\n}\n```\n\nIt can be placed on the field of a query and type. To custom your url queries, you can use the path parameter and another field with respectively, `params` and `item` variables.\n\n**Example**\n```js\ntype Country {\n name: String\n phone: String\n users: [User] @rest(url: \"http://foo.oto.tools/users/${item.name}\")\n}\n\ntype Query {\n user(id: String): User @rest(url: \"http://foo.oto.tools/users/${params.id}\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nGraphQL\n\n
      \n\n*Arguments : url, method, headers, timeout, query, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to call an other graphql server.\n\nThe required argument are the `url` and the `query`.\n\n**Example**\n```js\ntype Query {\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com/\", query: \"{ countries { name phone }}\")\n}\n\ntype Country {\n name: String\n phone: String\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nSoap\n\n
      \n*Arguments: all following arguments*\n\nThe soap directive is used to call a soap service. \n\n```js\ntype Query {\n randomNumber: String @soap(\n jq_response_filter: \".[\\\"soap:Envelope\\\"] | .[\\\"soap:Body\\\"] | .[\\\"m:NumberToWordsResponse\\\"] | .[\\\"m:NumberToWordsResult\\\"]\", \n url: \"https://www.dataaccess.com/webservicesserver/numberconversion.wso\", \n envelope: \" \\n \\n \\n \\n 12 \\n \\n \\n\")\n}\n```\n\n\n##### Specific arguments\n\n| Argument | Type | Optional | Default value |\n| --------------------------- | --------- | -------- | ------------- |\n| envelope | *STRING* | Required | |\n| url | *STRING* | x | |\n| action | *STRING* | x | |\n| preserve_query | *BOOLEAN* | Required | true |\n| charset | *STRING* | x | |\n| convert_request_body_to_xml | *BOOLEAN* | Required | true |\n| jq_request_filter | *STRING* | x | |\n| jq_response_filter | *STRING* | x | |\n\n@@@\n\n@@@div { .simple-block }\n\n
      \nJSON\n\n
      \n*Arguments: path, json, paginate*\n\nThe json directive can be used to expose static data or mocked data. The first usage is to defined a raw stringify JSON in the `data` argument. The second usage is to set data in the predefined field of the GraphQL plugin composer and to specify a path in the `path` argument.\n\n**Example**\n```js\ntype Query {\n users_from_raw_data: [User] @json(data: \"[{\\\"firstname\\\":\\\"Foo\\\",\\\"name\\\":\\\"Bar\\\"}]\")\n users_from_predefined_data: [User] @json(path: \"users\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nMock\n\n
      \n*Arguments: url*\n\nThe mock directive is to used with the Mock Responses Plugin, also named `Charlatan`. This directive can be interesting to mock your schema and start to use your Otoroshi route before starting to develop the underlying service.\n\n**Example**\n```js\ntype Query {\n users: @mock(url: \"/users\")\n}\n```\n\nThis example supposes that the Mock Responses plugin is set on the route's feed, and that an endpoint `/users` is available.\n\n@@@\n\n### List of directive arguments\n\n| Argument | Type | Optional | Default value |\n| ------------------ | ---------------- | --------------------------- | ------------- |\n| url | *STRING* | | |\n| method | *STRING* | x | GET |\n| headers | *STRING* | x | |\n| timeout | *INT* | x | 5000 |\n| data | *STRING* | x | |\n| path | *STRING* | x (only for json directive) | |\n| query | *STRING* | x | |\n| response_path | *STRING* | x | |\n| response_filter | *STRING* | x | |\n| limit | *INT* | x | |\n| offset | *INT* | x | |\n| value | *STRING* | | |\n| values | LIST of *STRING* | |\n| path | *STRING* | | |\n| paginate | *BOOLEAN* | x | |\n| unauthorized_value | *STRING* | x (only for permissions directive) | |\n"},{"name":"green-score.md","id":"/topics/green-score.md","url":"/topics/green-score.html","title":"Green Score","content":"# Green Score\n\nThe Green Score provide aggregated, quantitative data about the performance and behavior of an API over time. It is an aggregation of static and dynamic values that are coming from the usage of routes in Otoroshi. The main objective is to advise users on the consumption of their APIs and services.\n\n\n\nOtoroshi has a complete integration of the collective rules, divided into four concerns: **Architecture**, **Design**, **Usage** and **Logs retention**. The 6000 score points are spread over the four parts and a final note is given for each group of routes.\n\nThe API green score is available on 16.9.0 or later version of Otoroshi. You can find the feature on the search bar of your Otoroshi UI or directly in the sidebar by clicking on **Green score**.\n\nTo start the process, click on Add New Group, give a name and select a first route to audit. After clicking on the hammer icon, you can select the rules respected by your route. Before saving, you can adjust the values used to calculate the dynamic score. These thresholds are used to calculate a second green score depending on the amount of data you want not to exceed from your downstream service and the following other values: \n\n* **Overhead**: Otoroshi's calculation time to handle the request and response\n* **Duration**: the complete duration from the recpetion of the request by Otoroshi until the client gets a response\n* **Backend duration**: the time required for downstream service to respond to Otoroshi\n* **Calls**: the rate of calls by seconds\n* **Data in**: the amount of data received by the downstream service\n* **Data out**: the amount of data produced by the downstream service\n* **Headers in**: the amount of headers received by the downstream service\n* **Headers out**: the amount of headers produced by the downstream service\n\nThe Green Score works for all architectures, including simple leader or more advanced concept like [clustering](https://maif.github.io/otoroshi/manual/deploy/clustering.html)."},{"name":"http3.md","id":"/topics/http3.md","url":"/topics/http3.html","title":"HTTP3 support","content":"# HTTP3 support\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nHTTP3 server and client previews are available in otoroshi since version 1.5.14\n\n\n## Server\n\nto enable http3 server preview, you need to enable the following flags\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048\n```\n\nthen you will be able to send HTTP3 request on port 10048. For instance, using [quiche-client](https://github.com/cloudflare/quiche)\n\n```sh\ncargo run --bin quiche-client -- --no-verify 'https://my-service.oto.tools:10048'\n```\n\n## Client\n\nto consume services exposed with HTTP3, just select the `HTTP/3.0` protocol in the backend target."},{"name":"index.md","id":"/topics/index.md","url":"/topics/index.html","title":"Detailed topics","content":"# Detailed topics\n\nIn this sections, you will find informations about various Otoroshi topics \n\n* @ref:[Proxy engine](./engine.md)\n* @ref:[WASM support](./wasm-usage.md)\n* @ref:[Chaos engineering](./chaos-engineering.md)\n* @ref:[TLS](./tls.md)\n* @ref:[Otoroshi's PKI](./pki.md)\n* @ref:[Monitoring](./monitoring.md)\n* @ref:[Events and analytics](./events-and-analytics.md)\n* @ref:[Developer portal with Daikoku](./dev-portal.md)\n* @ref:[Sessions management](./sessions-mgmt.md)\n* @ref:[The Otoroshi communication protocol](./otoroshi-protocol.md)\n* @ref:[Expression language](./expression-language.md)\n* @ref:[Otoroshi user rights](./user-rights.md)\n* @ref:[GraphQL composer](./graphql-composer.md)\n* @ref:[Secret vaults](./secrets.md)\n* @ref:[Otoroshi tunnels](./tunnels.md)\n* @ref:[Relay routing](./relay-routing.md)\n* @ref:[Alternative http backend](./netty-server.md)\n* @ref:[HTTP3 support](./http3.md)\n* @ref:[Anonymous reporting](./anonymous-reporting.md)\n* @ref:[OpenTelemetry support](./opentelemetry.md)\n* @ref:[Green score](./green-score.md)\n\n@@@ index\n\n* [Proxy engine](./engine.md)\n* [WASM support](./wasm-usage.md)\n* [Chaos engineering](./chaos-engineering.md)\n* [TLS](./tls.md)\n* [Otoroshi's PKI](./pki.md)\n* [Monitoring](./monitoring.md)\n* [Events and analytics](./events-and-analytics.md)\n* [Developer portal with Daikoku](./dev-portal.md)\n* [Sessions management](./sessions-mgmt.md)\n* [The Otoroshi communication protocol](./otoroshi-protocol.md)\n* [Expression language](./expression-language.md)\n* [Otoroshi user rights](./user-rights.md)\n* [GraphQL composer](./graphql-composer.md)\n* [Secret vaults](./secrets.md)\n* [Otoroshi tunnels](./tunnels.md)\n* [Relay routing](./relay-routing.md)\n* [Alternative http backend](./netty-server.md)\n* [HTTP3 support](./http3.md)\n* [Anonymous reporting](./anonymous-reporting.md)\n* [OpenTelemetry support](./opentelemetry.md)\n* [Green score](./green-score.md)\n \n@@@\n"},{"name":"monitoring.md","id":"/topics/monitoring.md","url":"/topics/monitoring.html","title":"Monitoring","content":"# Monitoring\n\nThe Otoroshi API exposes two endpoints to know more about instance health. All the following endpoint are exposed on the instance host through it's ip address. It is also exposed on the otoroshi api hostname and the otoroshi backoffice hostname\n\n* `/health`: the health of the Otoroshi instance\n* `/metrics`: the metrics of the Otoroshi instance, either in JSON or Prometheus format using the `Accept` header (with `application/json` / `application/prometheus` values) or the `format` query param (with `json` or `prometheus` values)\n* `/live`: returns an http 200 response `{\"live\": true}` when the service is alive\n* `/ready`: return an http 200 response `{\"ready\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"ready\": false}`\n* `/startup`: return an http 200 response `{\"started\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"started\": false}`\n\nthose routes are also available on any hostname leading to otoroshi with a twist in the URL\n\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/health\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/metrics\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/live\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/ready\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/startup\n\n## Endpoints security\n\nThe two endpoints are exposed publicly on the Otoroshi admin api. But you can remove the corresponding public pattern and query the endpoints using standard apikeys. If you don't want to use apikeys but don't want to expose the endpoints publicly, you can defined two config. variables (`otoroshi.health.accessKey` or `HEALTH_ACCESS_KEY` and `otoroshi.metrics.accessKey` or `OTOROSHI_METRICS_ACCESS_KEY`) that will hold an access key for the endpoints. Then you can call the endpoints with an `access_key` query param with the value defined in the config. If you don't defined `otoroshi.metrics.accessKey` but define `otoroshi.health.accessKey`, `otoroshi.metrics.accessKey` will have the value of `otoroshi.health.accessKey`.\n \n## Examples\n\nlet say `otoroshi.health.accessKey` has value `MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY`\n\n```sh\n$ curl http://otoroshi-api.oto.tools:8080/health\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"otoroshi\":\"healthy\",\"datastore\":\"healthy\"}\n\n$ curl -H 'Accept: application/json' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"version\":\"4.0.0\",\"gauges\":{\"attr.app.commit\":{\"value\":\"xxxx\"},\"attr.app.id\":{\"value\":\"xxxx\"},\"attr.cluster.mode\":{\"value\":\"Leader\"},\"attr.cluster.name\":{\"value\":\"otoroshi-leader-0\"},\"attr.instance.env\":{\"value\":\"prod\"},\"attr.instance.id\":{\"value\":\"xxxx\"},\"attr.instance.number\":{\"value\":\"0\"},\"attr.jvm.cpu.usage\":{\"value\":136},\"attr.jvm.heap.size\":{\"value\":1409},\"attr.jvm.heap.used\":{\"value\":112},\"internals.0.concurrent-requests\":{\"value\":1},\"internals.global.throttling-quotas\":{\"value\":2},\"jvm.attr.name\":{\"value\":\"2085@xxxx\"},\"jvm.attr.uptime\":{\"value\":2296900},\"jvm.attr.vendor\":{\"value\":\"JDK11\"},\"jvm.gc.PS-MarkSweep.count\":{\"value\":3},\"jvm.gc.PS-MarkSweep.time\":{\"value\":261},\"jvm.gc.PS-Scavenge.count\":{\"value\":12},\"jvm.gc.PS-Scavenge.time\":{\"value\":161},\"jvm.memory.heap.committed\":{\"value\":1477967872},\"jvm.memory.heap.init\":{\"value\":1690304512},\"jvm.memory.heap.max\":{\"value\":3005218816},\"jvm.memory.heap.usage\":{\"value\":0.03916456777568639},\"jvm.memory.heap.used\":{\"value\":117698096},\"jvm.memory.non-heap.committed\":{\"value\":166445056},\"jvm.memory.non-heap.init\":{\"value\":7667712},\"jvm.memory.non-heap.max\":{\"value\":994050048},\"jvm.memory.non-heap.usage\":{\"value\":0.1523920694986979},\"jvm.memory.non-heap.used\":{\"value\":151485344},\"jvm.memory.pools.CodeHeap-'non-nmethods'.committed\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.max\":{\"value\":5832704},\"jvm.memory.pools.CodeHeap-'non-nmethods'.usage\":{\"value\":0.28408093398876405},\"jvm.memory.pools.CodeHeap-'non-nmethods'.used\":{\"value\":1656960},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.committed\":{\"value\":11796480},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.usage\":{\"value\":0.09536102872567315},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.used\":{\"value\":11721088},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.committed\":{\"value\":37355520},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.usage\":{\"value\":0.2538573047187417},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.used\":{\"value\":31202304},\"jvm.memory.pools.Compressed-Class-Space.committed\":{\"value\":14942208},\"jvm.memory.pools.Compressed-Class-Space.init\":{\"value\":0},\"jvm.memory.pools.Compressed-Class-Space.max\":{\"value\":367001600},\"jvm.memory.pools.Compressed-Class-Space.usage\":{\"value\":0.033858838762555805},\"jvm.memory.pools.Compressed-Class-Space.used\":{\"value\":12426248},\"jvm.memory.pools.Metaspace.committed\":{\"value\":99794944},\"jvm.memory.pools.Metaspace.init\":{\"value\":0},\"jvm.memory.pools.Metaspace.max\":{\"value\":375390208},\"jvm.memory.pools.Metaspace.usage\":{\"value\":0.25168142904782426},\"jvm.memory.pools.Metaspace.used\":{\"value\":94478744},\"jvm.memory.pools.PS-Eden-Space.committed\":{\"value\":349700096},\"jvm.memory.pools.PS-Eden-Space.init\":{\"value\":422576128},\"jvm.memory.pools.PS-Eden-Space.max\":{\"value\":1110966272},\"jvm.memory.pools.PS-Eden-Space.usage\":{\"value\":0.07505125052077188},\"jvm.memory.pools.PS-Eden-Space.used\":{\"value\":83379408},\"jvm.memory.pools.PS-Eden-Space.used-after-gc\":{\"value\":0},\"jvm.memory.pools.PS-Old-Gen.committed\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.init\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.max\":{\"value\":2253914112},\"jvm.memory.pools.PS-Old-Gen.usage\":{\"value\":0.014950035505168354},\"jvm.memory.pools.PS-Old-Gen.used\":{\"value\":33696096},\"jvm.memory.pools.PS-Old-Gen.used-after-gc\":{\"value\":23791152},\"jvm.memory.pools.PS-Survivor-Space.committed\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.init\":{\"value\":70254592},\"jvm.memory.pools.PS-Survivor-Space.max\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.usage\":{\"value\":0.59375},\"jvm.memory.pools.PS-Survivor-Space.used\":{\"value\":622592},\"jvm.memory.pools.PS-Survivor-Space.used-after-gc\":{\"value\":622592},\"jvm.memory.total.committed\":{\"value\":1644412928},\"jvm.memory.total.init\":{\"value\":1697972224},\"jvm.memory.total.max\":{\"value\":3999268864},\"jvm.memory.total.used\":{\"value\":269184904},\"jvm.thread.blocked.count\":{\"value\":0},\"jvm.thread.count\":{\"value\":82},\"jvm.thread.daemon.count\":{\"value\":11},\"jvm.thread.deadlock.count\":{\"value\":0},\"jvm.thread.deadlocks\":{\"value\":[]},\"jvm.thread.new.count\":{\"value\":0},\"jvm.thread.runnable.count\":{\"value\":25},\"jvm.thread.terminated.count\":{\"value\":0},\"jvm.thread.timed_waiting.count\":{\"value\":10},\"jvm.thread.waiting.count\":{\"value\":47}},\"counters\":{},\"histograms\":{},\"meters\":{},\"timers\":{}}\n\n$ curl -H 'Accept: application/prometheus' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n# TYPE attr_jvm_cpu_usage gauge\nattr_jvm_cpu_usage 83.0\n# TYPE attr_jvm_heap_size gauge\nattr_jvm_heap_size 1409.0\n# TYPE attr_jvm_heap_used gauge\nattr_jvm_heap_used 220.0\n# TYPE internals_0_concurrent_requests gauge\ninternals_0_concurrent_requests 1.0\n# TYPE internals_global_throttling_quotas gauge\ninternals_global_throttling_quotas 3.0\n# TYPE jvm_attr_uptime gauge\njvm_attr_uptime 2372614.0\n# TYPE jvm_gc_PS_MarkSweep_count gauge\njvm_gc_PS_MarkSweep_count 3.0\n# TYPE jvm_gc_PS_MarkSweep_time gauge\njvm_gc_PS_MarkSweep_time 261.0\n# TYPE jvm_gc_PS_Scavenge_count gauge\njvm_gc_PS_Scavenge_count 12.0\n# TYPE jvm_gc_PS_Scavenge_time gauge\njvm_gc_PS_Scavenge_time 161.0\n# TYPE jvm_memory_heap_committed gauge\njvm_memory_heap_committed 1.477967872E9\n# TYPE jvm_memory_heap_init gauge\njvm_memory_heap_init 1.690304512E9\n# TYPE jvm_memory_heap_max gauge\njvm_memory_heap_max 3.005218816E9\n# TYPE jvm_memory_heap_usage gauge\njvm_memory_heap_usage 0.07680553268571043\n# TYPE jvm_memory_heap_used gauge\njvm_memory_heap_used 2.30817432E8\n# TYPE jvm_memory_non_heap_committed gauge\njvm_memory_non_heap_committed 1.66510592E8\n# TYPE jvm_memory_non_heap_init gauge\njvm_memory_non_heap_init 7667712.0\n# TYPE jvm_memory_non_heap_max gauge\njvm_memory_non_heap_max 9.94050048E8\n# TYPE jvm_memory_non_heap_usage gauge\njvm_memory_non_heap_usage 0.15262878997416435\n# TYPE jvm_memory_non_heap_used gauge\njvm_memory_non_heap_used 1.51720656E8\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_nmethods__committed 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_nmethods__max 5832704.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_nmethods__usage 0.28408093398876405\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_nmethods__used 1656960.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__committed 1.1862016E7\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__usage 0.09610562183417755\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__used 1.1812608E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__committed 3.735552E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__usage 0.25493618368435084\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__used 3.1334912E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_committed gauge\njvm_memory_pools_Compressed_Class_Space_committed 1.4942208E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_init gauge\njvm_memory_pools_Compressed_Class_Space_init 0.0\n# TYPE jvm_memory_pools_Compressed_Class_Space_max gauge\njvm_memory_pools_Compressed_Class_Space_max 3.670016E8\n# TYPE jvm_memory_pools_Compressed_Class_Space_usage gauge\njvm_memory_pools_Compressed_Class_Space_usage 0.03386023385184152\n# TYPE jvm_memory_pools_Compressed_Class_Space_used gauge\njvm_memory_pools_Compressed_Class_Space_used 1.242676E7\n# TYPE jvm_memory_pools_Metaspace_committed gauge\njvm_memory_pools_Metaspace_committed 9.9794944E7\n# TYPE jvm_memory_pools_Metaspace_init gauge\njvm_memory_pools_Metaspace_init 0.0\n# TYPE jvm_memory_pools_Metaspace_max gauge\njvm_memory_pools_Metaspace_max 3.75390208E8\n# TYPE jvm_memory_pools_Metaspace_usage gauge\njvm_memory_pools_Metaspace_usage 0.25170985813247426\n# TYPE jvm_memory_pools_Metaspace_used gauge\njvm_memory_pools_Metaspace_used 9.4489416E7\n# TYPE jvm_memory_pools_PS_Eden_Space_committed gauge\njvm_memory_pools_PS_Eden_Space_committed 3.49700096E8\n# TYPE jvm_memory_pools_PS_Eden_Space_init gauge\njvm_memory_pools_PS_Eden_Space_init 4.22576128E8\n# TYPE jvm_memory_pools_PS_Eden_Space_max gauge\njvm_memory_pools_PS_Eden_Space_max 1.110966272E9\n# TYPE jvm_memory_pools_PS_Eden_Space_usage gauge\njvm_memory_pools_PS_Eden_Space_usage 0.17698545577448457\n# TYPE jvm_memory_pools_PS_Eden_Space_used gauge\njvm_memory_pools_PS_Eden_Space_used 1.96624872E8\n# TYPE jvm_memory_pools_PS_Eden_Space_used_after_gc gauge\njvm_memory_pools_PS_Eden_Space_used_after_gc 0.0\n# TYPE jvm_memory_pools_PS_Old_Gen_committed gauge\njvm_memory_pools_PS_Old_Gen_committed 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_init gauge\njvm_memory_pools_PS_Old_Gen_init 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_max gauge\njvm_memory_pools_PS_Old_Gen_max 2.253914112E9\n# TYPE jvm_memory_pools_PS_Old_Gen_usage gauge\njvm_memory_pools_PS_Old_Gen_usage 0.014950035505168354\n# TYPE jvm_memory_pools_PS_Old_Gen_used gauge\njvm_memory_pools_PS_Old_Gen_used 3.3696096E7\n# TYPE jvm_memory_pools_PS_Old_Gen_used_after_gc gauge\njvm_memory_pools_PS_Old_Gen_used_after_gc 2.3791152E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_committed gauge\njvm_memory_pools_PS_Survivor_Space_committed 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_init gauge\njvm_memory_pools_PS_Survivor_Space_init 7.0254592E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_max gauge\njvm_memory_pools_PS_Survivor_Space_max 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_usage gauge\njvm_memory_pools_PS_Survivor_Space_usage 0.59375\n# TYPE jvm_memory_pools_PS_Survivor_Space_used gauge\njvm_memory_pools_PS_Survivor_Space_used 622592.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_used_after_gc gauge\njvm_memory_pools_PS_Survivor_Space_used_after_gc 622592.0\n# TYPE jvm_memory_total_committed gauge\njvm_memory_total_committed 1.644478464E9\n# TYPE jvm_memory_total_init gauge\njvm_memory_total_init 1.697972224E9\n# TYPE jvm_memory_total_max gauge\njvm_memory_total_max 3.999268864E9\n# TYPE jvm_memory_total_used gauge\njvm_memory_total_used 3.82665128E8\n# TYPE jvm_thread_blocked_count gauge\njvm_thread_blocked_count 0.0\n# TYPE jvm_thread_count gauge\njvm_thread_count 82.0\n# TYPE jvm_thread_daemon_count gauge\njvm_thread_daemon_count 11.0\n# TYPE jvm_thread_deadlock_count gauge\njvm_thread_deadlock_count 0.0\n# TYPE jvm_thread_new_count gauge\njvm_thread_new_count 0.0\n# TYPE jvm_thread_runnable_count gauge\njvm_thread_runnable_count 25.0\n# TYPE jvm_thread_terminated_count gauge\njvm_thread_terminated_count 0.0\n# TYPE jvm_thread_timed_waiting_count gauge\njvm_thread_timed_waiting_count 10.0\n# TYPE jvm_thread_waiting_count gauge\njvm_thread_waiting_count 47.0\n```"},{"name":"netty-server.md","id":"/topics/netty-server.md","url":"/topics/netty-server.html","title":"Alternative HTTP server","content":"# Alternative HTTP server\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nwith the change of licence in Akka, we are experimenting around using Netty as http server for otoroshi (and getting rid of akka http)\n\nin `v1.5.14` we are introducing a new alternative http server base on [`reactor-netty`](https://projectreactor.io/docs/netty/release/reference/index.html). It also include a preview of an HTTP3 server using [netty-incubator-codec-quic](https://github.com/netty/netty-incubator-codec-quic) and [netty-incubator-codec-http3](https://github.com/netty/netty-incubator-codec-http3)\n\n## The specs\n\nthis new server can start during otoroshi boot sequence and accept HTTP/1.1 (with and without TLS), H2C and H2 (with and without TLS) connections and supporting both standard HTTP calls and websockets calls.\n\n## Enable the server\n\nto enable the server, just turn on the following flag\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\n```\n\nnow you should see something like the following in the logs\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Server options\n\nyou can also setup the host and ports of the server using\n\n```conf\notoroshi.next.experimental.netty-server.host = \"0.0.0.0\"\notoroshi.next.experimental.netty-server.http-port = 10049\notoroshi.next.experimental.netty-server.https-port = 10048\n```\n\nyou can also enable access logs using\n\n```conf\notoroshi.next.experimental.netty-server.accesslog = true\n```\n\nand enable wiretaping using \n\n```conf\notoroshi.next.experimental.netty-server.wiretap = true\n```\n\nyou can also custom number of worker thread using\n\n```conf\notoroshi.next.experimental.netty-server.thread = 0 # system automatically assign the right number of threads\n```\n\n## HTTP2\n\nyou can enable or disable HTTP2 with\n\n```conf\notoroshi.next.experimental.netty-server.http2.enabled = true\notoroshi.next.experimental.netty-server.http2.h2c = true\n```\n\n## HTTP3\n\nyou can enable or disable HTTP3 (preview ;) ) with\n\n```conf\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048 # yep can the the same as https because its on the UDP stack\n```\n\nthe result will be something like\n\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Native transport\n\nIt is possible to enable native transport for the server\n\n```conf\notoroshi.next.experimental.netty-server.native.enabled = true\notoroshi.next.experimental.netty-server.native.driver = \"Auto\"\n```\n\npossible values for `otoroshi.next.experimental.netty-server.native.driver` are \n\n- `Auto`: the server try to find the best native option available\n- `Epoll`: the server uses Epoll native transport for Linux environments\n- `KQueue`: the server uses KQueue native transport for MacOS environments\n- `IOUring`: the server uses IOUring native transport for Linux environments that supports it (experimental, using [netty-incubator-transport-io_uring](https://github.com/netty/netty-incubator-transport-io_uring))\n\nthe result will be something like when starting on a Mac\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - using KQueue native transport\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Env. variables\n\nyou can configure the server using the following env. variables\n\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER`\n\n"},{"name":"opentelemetry.md","id":"/topics/opentelemetry.md","url":"/topics/opentelemetry.html","title":"OpenTelemetry support","content":"# OpenTelemetry support\n\nOpenTelemetry is an open-source project focused on providing a set of APIs, libraries, agents, and instrumentation to \nenable observability in modern software applications. It helps developers and software teams collect, process, \nand export telemetry data, which includes metrics, traces, and logs, from their applications and infrastructure. \nThe project aims to provide a standardized approach to instrumenting applications for distributed tracing, metrics, and logging.\n\nHere's a breakdown of the key components of OpenTelemetry:\n\n- **Tracing**: Distributed tracing is a method used to monitor and understand the flow of requests across different services \nin a distributed system. OpenTelemetry allows developers to add instrumentation to their code to trace requests as they \nflow through various services, providing insights into performance bottlenecks and dependencies between components.\n- **Metrics**: Metrics are quantitative measurements that provide information about the behavior and performance of \nan application. OpenTelemetry enables developers to collect metrics from their applications, such as CPU usage, memory \nconsumption, and custom application-specific metrics, to gain visibility into the application's health and performance.\n- **Logging**: OpenTelemetry also supports capturing and exporting logs, which are textual records of events and messages \nthat occur during the execution of an application. Logs are essential for debugging and monitoring purposes, and \nOpenTelemetry allows developers to integrate logging with other telemetry data, making it easier to correlate events.\n\nOpenTelemetry is designed to be language-agnostic and vendor-agnostic, supporting multiple programming languages and \nvarious telemetry backends. This flexibility makes it easier for developers to adopt the OpenTelemetry standard \nregardless of their technology stack.\n\nThe goal of OpenTelemetry is to promote a consistent way of collecting telemetry data across different applications \nand environments, making it easier for developers to adopt observability best practices. By leveraging OpenTelemetry, \nsoftware teams can gain deeper insights into the behavior of their systems and improve performance, troubleshoot \nissues, and enhance the overall reliability of their applications.\n\nNow, OpenTelemetry is officialy supported in Otoroshi and can be used in different parts of your instance. You can use \nit to collect otoroshi server logs and otoroshi server metrics through config. file. Then you have access to 2 new data \nexporter that can export otoroshi events to OpenTelemetry log collector and send custom metrics to an OpenTelemetry metrics collector.\n\n## server logs\n\notoroshi server logs can be sent to an OpenTelemetry log collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-logs.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the logs push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-logs {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## server metrics\n\notoroshi server metrics can be sent to an OpenTelemetry metrics collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-metrics.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the metrics push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-metrics {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## logs data expoter\n\nA new kind of data exporter is now available to send otoroshi events serialized as text to an OpenTelemetry log collector. \nFirst create a new data exporter and select the type `otlp-logs`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\n@@@ div { .centered-img }\n\n@@@\n\n## metrics data exporter\n\nA new kind of data exporter is now available to send custom metrics derived from otoroshi events to an OpenTelemetry metrics collector. \nFirst create a new data exporter and select the type `otlp-metrics`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\nThen you will be able to add new metrics on this data exporter with a name, the type of metric (counter, timer, histogram), the value and the kind of event it's based on.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n\n\n"},{"name":"otoroshi-protocol.md","id":"/topics/otoroshi-protocol.md","url":"/topics/otoroshi-protocol.html","title":"The Otoroshi communication protocol","content":"# The Otoroshi communication protocol\n\nThe exchange protocol secure the communication with an app. When it's enabled, Otoroshi will send for each request a value in pre-selected token header, and will check the same header in the return request. On routes, you will have to use the `Otoroshi challenge token` plugin to enable it.\n\n### V1 challenge\n\nIf you enable secure communication for a given service with `V1 - simple values exchange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header and return it in a header named `Otoroshi-State-Resp`. \n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V1 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### V2 challenge\n\nIf you enable secure communication for a given service with `V2 - signed JWT token exhange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header value containing a JWT token, verify it's content signature then extract a claim named `state` and return a new JWT token in a header named `Otoroshi-State-Resp` with the `state` value in a claim named `state-resp`. By default, the signature algorithm is HMAC+SHA512 but can you can choose your own. The sent and returned JWT tokens have short TTL to avoid being replayed. You must be validate the tokens TTL. The audience of the response token must be `Otoroshi` and you have to specify `iat`, `nbf` and `exp`.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V2 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### Info. token\n\nOtoroshi is also sending a JWT token in a header named `Otoroshi-Claim` that the target app can validate too. On routes, you will have to use the `Otoroshi info. token` plugin to enable it.\n\nThe `Otoroshi-Claim` is a JWT token containing some informations about the service that is called and the client if available. You can choose between a legacy version of the token and a new one that is more clear and structured.\n\nBy default, the otoroshi jwt token is signed with the `otoroshi.claim.sharedKey` config property (or using the `$CLAIM_SHAREDKEY` env. variable) and uses the `HMAC512` signing algorythm. But it is possible to customize how the token is signed from the service descriptor page in the `Otoroshi exchange protocol` section. \n\n@@@ div { .centered-img }\n\n@@@\n\nusing another signing algo.\n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can choose the signing algorithm and the secret/keys used. You can use syntax like `${env.MY_ENV_VAR}` or `${config.my.config.path}` to provide secret/keys values. \n\nFor example, for a service named `my-service` with a signing key `secret` with `HMAC512` signing algorythm, the basic JWT token that will be sent should look like the following\n\n```\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiItLSIsImF1ZCI6Im15LXNlcnZpY2UiLCJpc3MiOiJPdG9yb3NoaSIsImV4cCI6MTUyMTQ0OTkwNiwiaWF0IjoxNTIxNDQ5ODc2LCJqdGkiOiI3MTAyNWNjMTktMmFjNy00Yjk3LTljYzctMWM0ODEzYmM1OTI0In0.mRcfuFVFPLUV1FWHyL6rLHIJIu0KEpBkKQCk5xh-_cBt9cb6uD6enynDU0H1X2VpW5-bFxWCy4U4V78CbAQv4g\n```\n\nif you decode it, the payload will look something like\n\n```json\n{\n \"sub\": \"apikey_client_id\",\n \"aud\": \"my-service\",\n \"iss\": \"Otoroshi\",\n \"exp\": 1521449906,\n \"iat\": 1521449876,\n \"jti\": \"71025cc19-2ac7-4b97-9cc7-1c4813bc5924\"\n}\n```\n\nIf you want to validate the `Otoroshi-Claim` on the target app side to ensure that the input requests only comes from `Otoroshi`, you will have to write an HTTP filter to do the job. For instance, if you want to write a filter to make sure that requests only comes from Otoroshi, you can write something like the following (using playframework 2.6).\n\nScala\n: @@snip [filter.scala](../snippets/filter.scala)\n\nJava\n: @@snip [filter.java](../snippets/filter.java)\n"},{"name":"pki.md","id":"/topics/pki.md","url":"/topics/pki.html","title":"Otoroshi's PKI","content":"# Otoroshi's PKI\n\nWith Otoroshi, you can add your own certificates, your own CA and even create self signed certificates or certificates from CAs. You can enable auto renewal of thoses self signed certificates or certificates generated. Certificates have to be created with the certificate chain and the private key in PEM format.\n\nAn Otoroshi instance always starts with 5 auto-generated certificates. \n\nThe highest certificate is the **Otoroshi Default Root CA Certificate**. This certificate is used by Otoroshi to sign the intermediate CA.\n\n**Otoroshi Default Intermediate CA Certificate**: first intermediate CA that must be used to issue new certificates in Otoroshi. Creating certificates directly from the CA root certificate increases the risk of root certificate compromise, and if the CA root certificate is compromised, the entire trust infrastructure built by the SSL provider will fail\n\nThis intermediate CA signed three certificates :\n\n* **Otoroshi Default Client certificate**: \n* **Otoroshi Default Jwt Signing Keypair**: default keypair (composed of a public and private key), exposed on `https://xxxxxx/.well-known/jwks.json`, that can be used to sign and verify JWT verifier\n* **Otoroshi Default Wildcard Certificate**: this certificate has `*.oto.tools` as common name. It can be very useful to the development phase\n\n## The PKI API\n\nThe Otoroshi's PKI can be managed using the admin api of otoroshi (by default admin api is exposed on https://otoroshi-api.xxxxx)\n\nLink to the complete swagger section about PKI : https://maif.github.io/otoroshi/swagger-ui/index.html#/pki\n\n* `POST` [/api/pki/certs/_letencrypt](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genLetsEncryptCert): generates a certificate using Let's Encrypt or any ACME compatible system\n* `POST` [/api/pki/certs/_p12](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.importCertFromP12): import a .p12 file as client certificates\n* `POST` [/api/pki/certs/_valid](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateIsValid): check if a certificate is valid (based on its own data)\n* `POST` [/api/pki/certs/_data](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateData): extract data from a certificate\n* `POST` [/api/pki/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCert): generates a self signed certificates\n* `POST` [/api/pki/csrs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCsr) : generates a CSR\n* `POST` [/api/pki/keys](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genKeyPair) : generates a keypair\n* `POST` [/api/pki/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCA) : generates a self signed CA\n* `POST` [/api/pki/cas/:ca/certs/_sign](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.signCert): sign a certificate based on CSR\n* `POST` [/api/pki/cas/:ca/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCert): generates a certificate\n* `POST` [/api/pki/cas/:ca/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSubCA) : generates a sub-CA\n\n## The PKI UI\n\nAll generated certificates are listed in the `https://xxxxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe PKI UI are composed of these following actions:\n\n* **Add item**: redirects the user on the certificate creation page. It’s useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* **Let's Encrypt certificate**: asks a certificate matching a given host to Let’s encrypt\n* **Create certificate**: issues a certificate with an existing Otoroshi certificate as CA. You can create a client certificate, a server certificate or a keypair certiciate that will be used to verify and sign JWT tokens.\n* **Import .p12 file**: loads a p12 file as certificate\n\nUnder these buttons, you have the list of current certificates, imported or generated, revoked or not. For each certificate, you will find: \n\n* a **name** \n* a **description** \n* the **subject** \n* the **type** of certificate (CA / client / keypair / certificate)\n* the **revoked reason** (empty if not) \n* the **creation date** following by its **expiration date**.\n\n## Exposed public keys\n\nThe Otoroshi certificate can be turned and used as keypair (simple action that can be executed by editing a certificate or during its creation, or using the admin api). A Otoroski keypair can be used to sign and verify JWT tokens with asymetric signature. Once a jwt token is signed with a keypair, it can be necessary to provide a way to the services to verify the tokens received by Otoroshi. This usage is cover by Otoroshi by the flag `Public key exposed`, available on each certificate.\n\nOtoroshi exposes each keypair with the flag enabled, on the following routes:\n\n* `https://xxxxxxxxx.xxxxxxx.xx/.well-known/otoroshi/security/jwks.json`\n* `https://otoroshi-api.xxxxxxx.xx/.well-known/jwks.json`\n\nOn these routes, you will find the list of public keys exposed using [the JWK standard](https://datatracker.ietf.org/doc/html/rfc7517)\n\n\n## OCSP Responder\n\nOtoroshi is able to revocate a certificate, directly from the UI, and to add a revocation status to specifiy the reason. The revocation reason can be :\n\n* `VALID`: The certificate is not revoked\n* `UNSPECIFIED`: Can be used to revoke certificates for reasons other than the specific codes.\n* `KEY_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `CA_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `AFFILIATION_CHANGED`: The subject's name or other information in the certificate has been modified but there is no cause to suspect that the private key has been compromised.\n* `SUPERSEDED`: The certificate has been superseded but there is no cause to suspect that the private key has been compromised\n* `CESSATION_OF_OPERATION`: The certificate is no longer needed for the purpose for which it was issued but there is no cause to suspect that the private key has been compromised\n* `CERTIFICATE_HOLD`: The certificate is temporarily revoked but there is no cause to suspect that the private kye has been compromised\n* `REMOVE_FROM_CRL`: The certificate has been unrevoked\n* `PRIVILEGE_WITH_DRAWN`: The certificate was revoked because a privilege contained within that certificate has been withdrawn\n* `AA_COMPROMISE`: It is known or suspected that aspects of the AA validated in the attribute certificate, have been compromised\n\nOtoroshi supports the Online Certificate Status Protocol for obtaining the revocation status of its certificates. The OCSP endpoint is also add to any generated certificate. This endpoint is available at `https://otoroshi-api.xxxxxx/.well-known/otoroshi/security/ocsp`\n\n## A.I.A : Authority Information Access\n\nOtoroshi provides a way to add the A.I.A in the certificate. This certificate extension contains :\n\n* Information about how to get the issuer of this certificate (CA issuer access method)\n* Address of the OCSP responder from where revocation of this certificate can be checked (OCSP access method)\n\n`https://xxxxxxxxxx/.well-known/otoroshi/security/certificates/:cert-id`"},{"name":"relay-routing.md","id":"/topics/relay-routing.md","url":"/topics/relay-routing.html","title":"Relay Routing","content":"# Relay Routing\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nRelay routing is the capability to forward traffic between otoroshi leader nodes based on network location of the target. Let say we have an otoroshi cluster split accross 3 network zones. Each zone has \n\n- one or more datastore instances\n- one or more otoroshi leader instances\n- one or more otoroshi worker instances\n\nthe datastores are replicated accross network zones in an active-active fashion. Each network zone also have applications, apis, etc deployed. Sometimes the same application is deployed in multiple zones, sometimes not. \n\nit can quickly become a nightmare when you want to access an application deployed in one network zone from another network zone. You'll have to publicly expose this application to be able to access it from the other zone. This pattern is fine, but sometimes it's not enough. With `relay routing`, you will be able to flag your routes as being deployed in one zone or another, and let otoroshi handle all the heavy lifting to route the traffic to the right network zone for you.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through relay nodes\n@@@\n\n## Otoroshi instance setup\n\nfirst of all, for every otoroshi instance deployed, you have to flag where the instance is deployed and, for leaders, how this instance can be contacted from other zones (this is a **MAJOR** requirement, without that, you won't be able to make relay routing work). Also, you'll have to enable the @ref:[new proxy engine](./engine.md).\n\nIn the otoroshi configuration file, for each instance, enable relay routing and configure where the instance is located and how the leader can be contacted\n\n```conf\notoroshi {\n ...\n cluster {\n mode = \"leader\" # or \"worker\" dependending on the instance kind\n ...\n relay {\n enabled = true # enable relay routing\n leaderOnly = true # use leaders as the only kind of relay node\n location { # you can use all those parameters at the same time. There is no actual network concepts bound here, just some kind of tagging system, so you can use it as you wish\n provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER}\n zone = \"zone-1\"\n region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION}\n datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER}\n rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK}\n }\n exposition {\n urls = [\"https://otoroshi-api-zone-1.my.domain:443\"]\n hostname = \"otoroshi-api-zone-1.my.domain\"\n clientId = \"apkid_relay-routing-apikey\"\n }\n }\n }\n}\n```\n\nalso, to make your leaders exposed by zone, do not hesitate to add domain names to the `otoroshi-admin-api` service and setup your DNS to bind those domains to the right place\n\n@@@ div { .centered-img }\n\n@@@\n\n## Route setup for an application deployed in only one zone\n\nNow, for any route/service deployed in only one zone, you will be able to flag it using its metadata as being deployed in one zone or another. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1`\n\n## Route setup for an application deployed in multiple zones at the same time\n\nNow, for any route/service deployed in multiple zones zones at the same time, you will be able to flag it using its metadata as being deployed in some zones. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1, zone-2` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1` or `zone-2`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1` or `zone-2` and load balance between them.\n\nalso, you will have to setup your targets to avoid trying to contact targets that are not actually in the current zone. To do that, you'll have to set the target predicate to `NetworkLocationMatch` and fill the possible locations according to the actual location of your target\n\n@@@ div { .centered-img }\n\n@@@\n\n## Demo\n\nyou can find a demo of this setup [here](https://github.com/MAIF/otoroshi/tree/master/demos/relay). This is a `docker-compose` setup with multiple network to simulate network zones. You also have an otoroshi export to understand how to setup your routes/services\n"},{"name":"secrets.md","id":"/topics/secrets.md","url":"/topics/secrets.html","title":"Secrets management","content":"# Secrets management\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSecrets are generally confidential values that should not appear in plain text in the application. There are several products that help you store, retrieve, and rotate these secrets securely. Otoroshi offers a mechanism to set up references to these secrets in its entities to benefits from the perks of your existing secrets management infrastructure. This feature only work with the @ref:[new proxy engine](./engine.md).\n\nA secret can be anything you want like an apikey secret, a certificate private key or password, a jwt verifier signing key, a password to a proxy, a value for a header, etc.\n\n## Enable secrets management in otoroshi\n\nBy default secrets management is disbaled. You can enable it by setting `otoroshi.vaults.enabled` or `${OTOROSHI_VAULTS_ENABLED}` to `true`.\n\n## Global configuration\n\nSecrets management can be only configured using otoroshi static configuration file (also using jvm args mechanism). \nThe configuration is located at `otoroshi.vaults` where you can find the global configuration of the secrets management system and the configurations for each enabled secrets management backends. Basically it looks like\n\n```conf\nvaults {\n enabled = false\n enabled = ${?OTOROSHI_VAULTS_ENABLED}\n secrets-ttl = 300000 # 5 minutes\n secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL}\n cached-secrets = 10000\n cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS}\n read-timeout = 10000 # 10 seconds\n read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT}\n # if enabled, only leader nodes fetches the secrets.\n # entities with secret values filled are then sent to workers when they poll the cluster state.\n # only works if `otoroshi.cluster.autoUpdateState=true`\n leader-fetch-only = false\n leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY}\n env {\n type = \"env\"\n prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX}\n }\n}\n```\n\nyou can see here the global configuration and a default backend configured that can retrieve secrets from environment variables. \n\nThe configuration keys can be used for \n\n- `secrets-ttl`: the amount of milliseconds before the secret value is read again from backend\n- `cached-secrets`: the number of secrets that will be cached on an otoroshi instance\n- `read-timeout`: the timeout (in milliseconds) to read a secret from a backend\n\n## Entities with secrets management\n\nthe entities that support secrets management are the following \n\n- `routes`\n- `services`\n- `service_descriptors`\n- `apikeys`\n- `certificates`\n- `jwt_verifiers`\n- `authentication_modules`\n- `targets`\n- `backends`\n- `tcp_services`\n- `data_exporters`\n\n## Define a reference to a secret\n\nin the previously listed entities, you can define, almost everywhere, references to a secret using the following syntax:\n\n`${vault://name_of_the_vault/secret/of/the/path}`\n\nlet say I define a new apikey with the following value as secret `${vault://my_env/apikey_secret}` with the following secrets management configuration\n\n```conf\nvaults {\n enabled = true\n secrets-ttl = 300000\n cached-secrets = 10000\n read-ttl = 10000\n my_env {\n type = \"env\"\n }\n}\n```\n\nif the machine running otoroshi has an environment variable named `APIKEY_SECRET` with the value `verysecret`, then you will be able to can an api with the defined apikey `client_id` and a `client_secret` value of `verysecret`\n\n```sh\ncurl 'http://my-awesome-api.oto.tools:8080/api/stuff' -u awesome_apikey:verysecret\n```\n\n## Possible backends\n\nOtoroshi comes with the support of several secrets management backends.\n\n### Environment variables\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"env\"\n prefix = \"the_prefix_added_to_the_name_of_the_env_variable\"\n }\n}\n```\n\n### Local\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"local\"\n root = \"the_root_path/in_otoroshi/environment\"\n }\n}\n```\n\nvalue of this vault can be configured in the danger zone > Global metadata > Otoroshi environment.\n\n### Infisical\n\na backend for the awesome open source project [Infisical](https://infisical.com/). It support both E2EE and non E2EE secrets.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"infisical\"\n baseUrl = \"https://app.infisical.com\" # optional, the base url of your infisical server, fallbacks to https://app.infisical.com\n serviceToken = \"st.xxxx.yyyy.zzzz\" # the service token for your projet\n e2ee = true # are you secrets end to end encrypted\n defaultSecretType = \"shared\" # optional, fallbacks to shared\n defaultWorkspaceId = \"xxxxxx\" # optional, value can be passed in the secret address\n defaultEnvironment = \"dev\" # optional, value can be passed in the secret address\n }\n}\n```\n\nyou should define your references like `${vault://infisical_vault/my_secret_path?workspaceId=xxx&environment=dev&type=shared}`. `workspaceId`, `environment` and `type` are optional if filled in global config. \n\nYou can also pass a `json_pointer=/foo/bar` to handle the value like a json document a select a value inside it.\n\n### Hashicorp Vault\n\na backend for [Hashicorp Vault](https://www.vaultproject.io/). Right now we only support KV engines.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"hashicorp-vault\"\n url = \"http://127.0.0.1:8200\"\n mount = \"kv\" # the name of the secret store in vault\n kv = \"v2\" # the version of the kv store (v1 or v2)\n token = \"root\" # the token that can access to your secrets\n }\n}\n```\n\nyou should define your references like `${vault://hashicorp_vault/secret/path/key_name}`.\n\n\n### Azure Key Vault\n\na backend for [Azure Key Vault](https://azure.microsoft.com/en-en/services/key-vault/). Right now we only support secrets and not keys and certificates.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"azure\"\n url = \"https://keyvaultname.vault.azure.net\"\n api-version = \"7.2\" # the api version of the vault\n tenant = \"xxxx-xxx-xxx\" # your azure tenant id, optional\n client_id = \"xxxxx\" # your azure client_id\n client_secret = \"xxxxx\" # your azure client_secret\n # token = \"xxx\" possible if you have a long lived existing token. will take over tenant / client_id / client_secret\n }\n}\n```\n\nyou should define your references like `${vault://azure_vault/secret_name/secret_version}`. `secret_version` is mandatory\n\nIf you want to use certificates and keys objects from the azure key vault, you will have to specify an option in the reference named `azure_secret_kind` with possible value `certificate`, `privkey`, `pubkey` like the following :\n\n```\n${vault://azure_vault/myprivatekey/secret_version?azure_secret_kind=privkey}\n```\n\n### AWS Secrets Manager\n\na backend for [AWS Secrets Manager](https://aws.amazon.com/en/secrets-manager/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"aws\"\n access-key = \"key\"\n access-key-secret = \"secret\"\n region = \"eu-west-3\" # the aws region of your secrets management\n }\n}\n```\n\nyou should define your references like `${vault://aws_vault/secret_name/secret_version}`. `secret_version` is optional\n\n### Google Cloud Secrets Manager\n\na backend for [Google Cloud Secrets Manager](https://cloud.google.com/secret-manager)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"gcloud\"\n url = \"https://secretmanager.googleapis.com\"\n apikey = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://gcloud_vault/projects/foo/secrets/bar/versions/the_version}`. `the_version` can be `latest`\n\n### AlibabaCloud Cloud Secrets Manager\n\na backend for [AlibabaCloud Secrets Manager](https://www.alibabacloud.com/help/en/doc-detail/152001.html)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"alibaba-cloud\"\n url = \"https://kms.eu-central-1.aliyuncs.com\"\n access-key-id = \"access-key\"\n access-key-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://alibaba_vault/secret_name}`\n\n\n### Kubernetes Secrets\n\na backend for [Kubernetes secrets](https://kubernetes.io/en/docs/concepts/configuration/secret/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"kubernetes\"\n # see the configuration of the kubernetes plugin, \n # by default if the pod if well configured, \n # you don't have to setup anything\n }\n}\n```\n\nyou should define your references like `${vault://k8s_vault/namespace/secret_name/key_name}`. `key_name` is optional. if present, otoroshi will try to lookup `key_name` in the secrets `stringData`, if not defined the secrets `data` will be base64 decoded and used.\n\n\n### Izanami config.\n\na backend for [Izanami config.](https://maif.github.io/izanami/manual/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"izanami\"\n url = \"http://127.0.0.1:8200\"\n client-id = \"client\"\n client-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://izanami_vault/the:secret:id/key_name}`. `key_name` is optional if the secret value is not a json object\n\n### Spring Cloud Config\n\na backend for [Spring Cloud Config.](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"spring-cloud\"\n url = \"http://127.0.0.1:8000\"\n root = \"myapp/prod\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://spring_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n\n### Http backend\n\na backend for that uses the result of an http endpoint\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"http\"\n url = \"http://127.0.0.1:8000/endpoint/for/config\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://http_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n"},{"name":"sessions-mgmt.md","id":"/topics/sessions-mgmt.md","url":"/topics/sessions-mgmt.html","title":"Sessions management","content":"# Sessions management\n\n## Admins\n\nAll logged users to an Otoroshi instance are administrators. An user session is created for each sucessfull connection to the UI. \n\nThese sessions are listed in the `Admin users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/admin`).\n\nAn admin user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Rights`: list of rules to authorize the connected user on each tenant and teams.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n\nIn the `Admin users sessions` page, you have two more actions:\n\n* `Discard all sessions`: kills all current sessions (including the session of the owner of this action)\n* `Discard old sessions`: kill all outdated sessions\n\n## Private apps\n\nAll logged users to a protected application has an private user session.\n\nThese sessions are listed in the `Private apps users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/private`).\n\nAn private user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Meta.`: list of metadatas added by the authentication module.\n* `Tokens`: list of tokens received from the identity provider used. In the case of a memory authentication, this part will keep empty.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n"},{"name":"tls.md","id":"/topics/tls.md","url":"/topics/tls.html","title":"TLS","content":"# TLS\n\nas you might have understand, otoroshi can store TLS certificates and use them dynamically. It means that once a certificate is imported or created in otoroshi, you can immediately use it to serve http request over TLS, to call https backends that requires mTLS or that do not have certicates signed by a globally knowned authority.\n\n## TLS termination\n\nany certficate added to otoroshi with a valid `CN` and `SANs` can be used in the following seconds to serve https requests. If you do not provide a private key with a certificate chain, the certificate will only be trusted like a CA. If you want to perform mTLS calls on you otoroshi instance, do not forget to enabled it (it is disabled by default for performance reasons as the TLS handshake is bigger with mTLS enabled)\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\n### TLS termination configuration\n\nYou can configure TLS termination statically using config. file or env. variables. Everything is available at `otoroshi.tls`\n\n```conf\notoroshi {\n tls {\n # the cipher suites used by otoroshi TLS termination\n cipherSuitesJDK11 = [\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuitesJDK8 = [\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_RSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuites = []\n # the protocols used by otoroshi TLS termination\n protocolsJDK11 = [\"TLSv1.3\", \"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"]\n protocolsJDK8 = [\"SSLv2Hello\", \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n protocols = []\n # the JDK cacert access\n cacert {\n path = \"$JAVA_HOME/lib/security/cacerts\"\n password = \"changeit\"\n }\n # the mtls mode\n fromOutside {\n clientAuth = \"None\"\n clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH}\n }\n # the default trust mode\n trust {\n all = false\n all = ${?OTOROSHI_SSL_TRUST_ALL}\n }\n # some initial cacert access, useful to include non standard CA when starting (file paths)\n initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT}\n initialCacert = ${?INITIAL_CACERT}\n initialCert = ${?CLUSTER_WORKER_INITIAL_CERT}\n initialCert = ${?INITIAL_CERT}\n initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY}\n initialCertKey = ${?INITIAL_CERT_KEY}\n # initialCerts = [] \n }\n}\n```\n\n\n### TLS termination settings\n\nIt is possible to adjust the behavior of the TLS termination from the `danger zone` at the `Tls Settings` section. Here you can either define that a non-matching SNI call will use a random TLS certtificate to reply or will use a default domain (the TLS certificate associated to this domain) to reply. Here you can also choose if you want to trust all the CAs trusted by your JDK when performing TLS calls `Trust JDK CAs (client)` or when receiving mTLS calls `Trust JDK CAs (server)`. If you disable the later, it is possible to select the list of CAs presented to the client during mTLS handshake.\n\n### Certificates auto generation\n\nit is also possible to generate non-existing certificate on the fly without losing the request. If you are interested by this feature, you can enable it in the `danger zone` at the `Auto Generate Certificates` section. Here you'll have to enable it and select the CA that will generate the certificate. Of course, the client will have to trust the selected CA. You can also add filters to choose which domain are allowed to generate certificates or not. The `Reply Nicely` flag is used to reply a nice error message (ie. human readable) telling that it's not possible to have an auto certficate for the current domain. \n\n## Backends TLS and mTLS calls\n\nFor any call to a backend, it is possible to customize the TLS behavior \n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can define your level of trust (trust all, loose verification) or even select on or more CAs you will trust for the following backend calls. You can also select the client certificate that will be used for the following backend calls\n\n## Keypair for signing and verification\n\nIt is also possible to use the keypair contained in a certificate to sign and verificate JWT token signature. You can mark an existing certificate in otoroshi as a keypair using the `keypair` on the certificate page.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"tunnels.md","id":"/topics/tunnels.md","url":"/topics/tunnels.html","title":"Otoroshi tunnels","content":"# Otoroshi tunnels\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSometimes, exposing apis that lives in our private network can be a nightmare, especially from a networking point of view. \nWith otoroshi tunnels, this is now trivial, as long as your internal otoroshi (that lives inside your private network) is able to contact an external otoroshi (exposed on the internet).\n\n@@@ warning { .margin-top-20 }\nYou have to enable cluster mode (Leader or Worker) to make this feature work. As this feature is experimental, we only support simple http request right now. Server Sent Event and Websocket request are not supported at the moment.\n@@@\n\n## How Otoroshi tunnels works\n\nthe main idea behind otoroshi tunnels is that the connection between your private network et the public network is initiated by the private network side. You don't have to expose a part of your private network, create a DMZ or whatever, you just have to authorize your private network otoroshi instance to contact your public network otoroshi instance.\n\n@@@ div { .centered-img }\n\n@@@\n\nonce the persistent tunnel has been created, you can create routes on the public otoroshi instance that uses the otoroshi `Remote tunnel calls` to target your remote routes through the designated tunnel instance \n\n\n@@@ div { .centered-img }\n\n@@@\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through otoroshi tunnels\n@@@\n\n## Otoroshi tunnel example\n\nfirst you have to enable the tunnels feature in your otoroshi configuration (on both public and private instances)\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n enabled = ${?OTOROSHI_TUNNELS_ENABLED}\n ...\n }\n}\n```\n\nthen you can setup a tunnel instance on your private instance to contact your public instance\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n ...\n public-apis {\n id = \"public-apis\"\n name = \"public apis tunnel\"\n url = \"https://otoroshi-api.company.com:443\"\n host = \"otoroshi-api.company.com\"\n clientId = \"xxx\"\n clientSecret = \"xxxxxx\"\n # ipAddress = \"127.0.0.1\" # optional: ip address of the public instance admin api\n # tls { # optional: TLS settings to access the public instance admin api\n # ... \n # }\n # export-routes = true # optional: send routes information to remote otoroshi instance to facilitate remote route exposition\n # export-routes-tag = \"tunnel-exposed\" # optional: only send routes information if the route has this tag\n }\n }\n}\n```\n\nNow when your private otoroshi instance will boot, a persistent tunnel will be made between private and public instance. \nNow let say you have a private api exposed on `api-a.company.local` on your private otoroshi instance and you want to expose it on your public otoroshi instance. \n\nFirst create a new route exposed on `api-a.company.com` that targets `https://api-a.company.local:443`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen add the `Remote tunnel calls` plugin to your route and set the tunnel id to `public-apis` to match the id you set in the otoroshi config file\n\n@@@ div { .centered-img }\n\n@@@\n\nadd all the plugin you need to secure this brand new public api and call it\n\n```sh\ncurl \"https://api-a.company.com/users\" | jq\n```\n\n## Easily expose your remote services\n\nyou can see all the connected tunnel instances on an otoroshi instance on the `Connected tunnels` (`Cog icon` / `Connected tunnels`). For each tunnel instance you will be able to check the tunnel health and also to easily expose all the routes available on the other end of the tunnel. Just clic on the `expose` button of the route you want to expose, and a new route will be created with the `Remote tunnel calls` plugin already setup.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"user-rights.md","id":"/topics/user-rights.md","url":"/topics/user-rights.html","title":"Otoroshi user rights","content":"# Otoroshi user rights\n\nIn Otoroshi, all users are considered **Administrators**. This choice is reinforced by the fact that Otoroshi is designed to be an administrator user interface and not an interface for users who simply want to view information. For this type of use, we encourage to use the admin API rather than giving access to the user interface.\n\nThe Otoroshi rights are split by a list of authorizations on **organizations** and **teams**. \n\nLet's taking an example where we want to authorize an administrator user on all organizations and teams.\n\nThe list of rights will be :\n\n```json\n[\n {\n \"tenant\": \"*:rw\", # (1)\n \"teams\": [\"*:rw\"] # (2)\n }\n]\n```\n\n* (1): this field, separated by a colon, indicates the name of the tenant and the associated rights. In our case, we set `*` to apply the rights to all tenants, and the `rw` to get the read and write access on them.\n* (2): the `teams` array field, represents the list of rights, applied by team. The behaviour is the same as the tenant field, we define the team or the wildcard, followed by the rights\n\nif you want to have an user that is administrator only for one organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that is administrator only for two organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that can only see 3 teams of one organization and one team in the other, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\n \"team-1:rw\",\n \"team-2:rw\",\n \"team-3:rw\",\n ]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\n \"team-4:rw\"\n ]\n }\n]\n```\n\nThe list of possible rights for an organization or a team is:\n\n* **r**: read access\n* **w**: write access\n* **not**: none access to the resource\n\nThe list of possible tenant and teams are your created tenants and teams, and the wildcard to define rights to all resources once.\n\nThe user rights is defined by the @ref:[authentication modules](../entities/auth-modules.md).\n"},{"name":"wasm-usage.md","id":"/topics/wasm-usage.md","url":"/topics/wasm-usage.html","title":"Otoroshi and WASM","content":"# Otoroshi and WASM\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. These plugins are:\n\n- `WasmRouteMatcher`: useful to define if a route can handle a request\n- `WasmPreRoute`: useful to check request and extract useful stuff for the other plugins\n- `WasmAccessValidator`: useful to control access to a route (jump to the next section to learn more about it)\n- `WasmRequestTransformer`: transform the content of an incoming request (body, headers, etc ...)\n- `WasmBackend`: execute a WASM file as Otoroshi target. Useful to implement user defined logic and function at the edge\n- `WasmResponseTransformer`: transform the content of the response produced by the target\n- `WasmSink`: create a sink plugin to handle unmatched requests\n- `WasmRequestHandler`: create a plugin that can handle the whole request lifecycle\n- `WasmJob`: create a job backed by a wasm function\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Available tutorials\n\nhere is the list of available tutorials about wasm in Otoroshi\n\n1. @ref:[Install a Wasmo](../how-to-s/wasmo-installation.md)\n2. @ref:[Use a WASM plugin](../how-to-s/wasm-usage.md)\n\n## Wasm plugins entities\n\nOtoroshi provides a dedicated entity for wasm plugins. Those entities makes it easy to declare a wasm plugin with specific configuration only once and use it in multiple places. \n\nYou can find wasm plugin entities at `/bo/dashboard/wasm-plugins`\n\nIn a wasm plugin entity, you can define the source of your wasm plugin. You can choose between\n\n- `base64`: a base64 encoded wasm script\n- `file`: the path to a wasm script file\n- `http`: the url to a wasm script file\n- `wasmo`: the name of a wasm script compiled by a Wasmo instance\n\nthen you can define the number of memory pages available for each plugin instanciation, the name of the function you want to invoke, the config. map of the VM and if you want to keep a wasm vm alive during the request lifecycle to be able to reuse it in different plugin steps\n\n@@@ div { .centered-img }\n\n@@@\n\n## Otoroshi plugins api\n\nthe following parts illustrates the apis for the different plugins. Otoroshi uses [Extism](https://extism.org/) to handle content sharing between the JVM and the wasm VM. All structures are sent to/from the plugins as json strings. \n\nfor instance, if we want to write a `WasmBackendCall` plugin using javascript, we could write something like\n\n```js\nfunction backend_call() {\n const input_str = Host.inputString(); // here we get the context passed by otoroshi as json string\n const backend_call_context = JSON.parse(input_str); // and parse it\n if (backend_call_context.path === '/hello') {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n message: `Hello ${ctx.request.query.name[0]}!` \n },\n status: 200,\n }));\n } else {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n error: \"not found\"\n },\n status: 404,\n }));\n }\n return 0; // we return 0 to tell otoroshi that everything went fine\n}\n```\n\nthe following examples are written in rust. the rust macros provided by extism makes the usage of `Host.inputString` and `Host.outputString` useless. Remember that it's still used under the hood and that the structures are passed as json strings.\n\ndo not forget to add the extism pdk library to your project to make it compile\n\nCargo.toml\n: @@snip [Cargo.toml](../snippets/wasmo/Cargo.toml) \n\ngo.mod\n: @@snip [go.mod](../snippets/wasmo/go.mod) \n\npackage.json\n: @@snip [package.json](../snippets/wasmo/package.json) \n\n### WasmRouteMatcher\n\nA route matcher is a plugin that can help the otoroshi router to select a route instance based on your own custom predicate. Basically it's a function that returns a boolean answer.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn matches_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteResponse {\n pub result: bool,\n}\n```\n\n### WasmPreRoute\n\nA pre-route plugin can be used to short-circuit a request or enrich it (maybe extracting your own kind of auth. token, etc) a the very beginning of the request handling process, just after the routing part, when a route has bee chosen by the otoroshi router.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn pre_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteResponse {\n pub error: bool,\n pub attrs: Option>,\n pub status: Option,\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmAccessValidator\n\nAn access validator plugin is typically used to verify if the request can continue or must be cancelled. For instance, the otoroshi apikey plugin is an access validator that check if the current apikey provided by the client is legit and authorized on the current route.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_access(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorContext {\n pub snowflake: Option,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorError {\n pub message: String,\n pub status: u32,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorResponse {\n pub result: bool,\n pub error: Option,\n}\n```\n\n### WasmRequestTransformer\n\nA request transformer plugin can be used to compose or transform the request that will be sent to the backend\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmRequestTransformerContext {\n pub snowflake: Option,\n pub raw_request: OtoroshiRequest,\n pub otoroshi_request: OtoroshiRequest,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n}\n```\n\n### WasmBackendCall\n\nA backend call plugin can be used to simulate a backend behavior in otoroshi. For instance the static backend of otoroshi return the content of a file\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn call_backend(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmBackendContext {\n pub snowflake: Option,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub raw_request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n pub request: OtoroshiRequest,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmBackendResponse {\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n pub status: u32,\n}\n```\n\n### WasmResponseTransformer\n\nA response transformer plugin can be used to compose or transform the response that will be sent back to the client\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_response(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmResponseTransformerContext {\n pub snowflake: Option,\n pub raw_response: OtoroshiResponse,\n pub otoroshi_response: OtoroshiResponse,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub response_body_bytes: Option>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmTransformerResponse {\n pub headers: HashMap,\n pub cookies: Value,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmSink\n\nA sink is a kind of plugin that can be used to respond to any unmatched request before otoroshi sends back a 404 response\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn sink_matches(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn sink_handle(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkContext {\n pub snowflake: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub origin: String,\n pub status: u32,\n pub message: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkMatchesResponse {\n pub result: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkHandleResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmRequestHandler\n\nA request handler is a very special kind of plugin that can bypass the otoroshi proxy engine on specific domains and completely handles the request/response lifecycle on it's own.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerContext {\n pub request: RawRequest\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmJob\n\nA job is a plugin that can run periodically an do whatever you want. Typically, the kubernetes plugins of otoroshi are jobs that periodically sync stuff between otoroshi and kubernetes using the kube-api\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn job_run(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobContext {\n pub attrs: Value,\n pub global_config: Value,\n pub snowflake: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobResult {\n\n}\n```\n\n### Common types\n\n```rs\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Backend {\n pub id: String,\n pub hostname: String,\n pub port: u32,\n pub tls: bool,\n pub weight: u32,\n pub protocol: String,\n pub ip_address: Option,\n pub predicate: Value,\n pub tls_config: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Apikey {\n #[serde(alias = \"clientId\")]\n pub client_id: String,\n #[serde(alias = \"clientName\")]\n pub client_name: String,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct User {\n pub name: String,\n pub email: String,\n pub profile: Value,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RawRequest {\n pub id: u32,\n pub method: String,\n pub headers: HashMap,\n pub cookies: Value,\n pub tls: bool,\n pub uri: String,\n pub path: String,\n pub version: String,\n pub has_body: bool,\n pub remote: String,\n pub client_cert_chain: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Frontend {\n pub domains: Vec,\n pub strict_path: Option,\n pub exact: bool,\n pub headers: HashMap,\n pub query: HashMap,\n pub methods: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct HealthCheck {\n pub enabled: bool,\n pub url: String,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RouteBackend {\n pub targets: Vec,\n pub root: String,\n pub rewrite: bool,\n pub load_balancing: Value,\n pub client: Value,\n pub health_check: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Route {\n pub id: String,\n pub name: String,\n pub description: String,\n pub tags: Vec,\n pub metadata: HashMap,\n pub enabled: bool,\n pub debug_flow: bool,\n pub export_reporting: bool,\n pub capture: bool,\n pub groups: Vec,\n pub frontend: Frontend,\n pub backend: RouteBackend,\n pub backend_ref: Option,\n pub plugins: Value,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct OtoroshiResponse {\n pub status: u32,\n pub headers: HashMap,\n pub cookies: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct OtoroshiRequest {\n pub url: String,\n pub method: String,\n pub headers: HashMap,\n pub version: String,\n pub client_cert_chain: Value,\n pub backend: Option,\n pub cookies: Value,\n}\n```\n\n## Otoroshi interop. with host functions\n\notoroshi provides some host function in order make wasm interact with otoroshi internals. You can\n\n- access wasi resources\n- access http resources\n- access otoroshi internal state\n- access otoroshi internal configuration\n- access otoroshi static configuration\n- access plugin scoped in-memory key/value storage\n- access global in-memory key/value storage\n- access plugin scoped persistent key/value storage\n- access global persistent key/value storage\n\n### authorizations\n\nall the previously listed host functions are enabled with specific authorizations to avoid security issues with third party plugins. You can enable/disable the host function from the wasm plugin entity\n\n@@@ div { .centered-img }\n\n@@@\n\n\n### host functions abi\n\nyou'll find here the raw signatures for the otoroshi host functions. we are currently in the process of writing higher level functions to hide the complexity.\n\nevery time you the the following signature: `(context: u64, size: u64) -> u64` it means that otoroshi is expecting for a pointer to the call context (which is a json string) and it's size. The return is a pointer to the response (which is a json string).\n\nthe signature `(unused: u64) -> u64` means that there is no need for a params but as we technically need one (and hope to don't need one in the future), you have to pass something like `0` as parameter.\n\n```rust\nextern \"C\" {\n // log messages in otoroshi (log levels are 0 to 6 for trace, debug, info, warn, error, critical, max)\n fn proxy_log(logLevel: i32, message: u64, size: u64) -> i32;\n // trigger an otoroshi wasm event that can be exported through data exporters\n fn proxy_log_event(context: u64, size: u64) -> u64;\n // an http client\n fn proxy_http_call(context: u64, size: u64) -> u64;\n // access the current otoroshi state containing a snapshot of all otoroshi entities\n fn proxy_state(context: u64) -> u64;\n fn proxy_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi cluster configuration\n fn proxy_cluster_state(context: u64) -> u64;\n fn proxy_cluster_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi static configuration\n fn proxy_global_config(unused: u64) -> u64;\n // access the current otoroshi dynamic configuration\n fn proxy_config(unused: u64) -> u64;\n // access a persistent key/value store shared by every wasm plugins\n fn proxy_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_datastore_all_matching(context: u64, size: u64) -> u64;\n // access a persistent key/value store for the current plugin instance only\n fn proxy_plugin_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_all_matching(context: u64, size: u64) -> u64;\n // access an in memory key/value store for the current plugin instance only\n fn proxy_plugin_map_set(context: u64, size: u64) -> u64;\n fn proxy_plugin_map_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_map(unused: u64) -> u64;\n // access an in memory key/value store shared by every wasm plugins\n fn proxy_global_map_set(context: u64, size: u64) -> u64;\n fn proxy_global_map_get(context: u64, size: u64) -> u64;\n fn proxy_global_map(unused: u64) -> u64;\n}\n```\n\nright know, when using the Wasmo, a default idiomatic implementation is provided for `TinyGo` and `Rust`\n\nhost.rs\n: @@snip [host.rs](../snippets/wasmo/host.rs) \n\nhost.go\n: @@snip [host.go](../snippets/wasmo/host.go) \n"}] \ No newline at end of file +[{"name":"about.md","id":"/about.md","url":"/about.html","title":"About Otoroshi","content":"# About Otoroshi\n\nAt the beginning of 2017, we had the need to create a new environment to be able to create new \"digital\" products very quickly in an agile fashion at @link:[MAIF](https://www.maif.fr) { open=new }. Naturally we turned to PaaS solutions and chose the excellent @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } product to run our apps. \n\nWe also chose that every feature team will have the freedom to choose its own technological stack to build its product. It was a nice move but it has also introduced some challenges in terms of homogeneity for traceability, security, logging, ... because we did not want to force library usage in the products. We could have used something like @link:[Service Mesh Pattern](http://philcalcado.com/2017/08/03/pattern_service_mesh.html) { open=new } but the deployement model of @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } prevented us to do it.\n\nThe right solution was to use a reverse proxy or some kind of API Gateway able to provide tracability, logging, security with apikeys, quotas, DNS as a service locator, etc. We needed something easy to use, with a human friendly UI, a nice API to extends its features, true hot reconfiguration, able to generate internal events for third party usage. A couple of solutions were available at that time, but not one seems to fit our needs, there was always something missing, too complicated for our needs or not playing well with @link:[Clever Cloud](https://www.clever-cloud.com) { open=new } deployment model.\n\nAt some point, we tried to write a small prototype to explore what could be our dream reverse proxy. The design was very simple, there were some rough edges but every major feature needed was there waiting to be enhanced.\n\n**Otoroshi** was born and we decided to move ahead with our hairy monster :)\n\n## Philosophy \n\nEvery OSS product build at @link:[MAIF](https://www.maif.fr) { open=new } like the developer portal @link:[Daikoku](https://maif.github.io/daikoku) { open=new } or @link:[Izanami](https://maif.github.io/izanami) { open=new } follow a common philosophy. \n\n* the services or API provided should be **technology agnostic**.\n* **http first**: http is the right answer to the previous quote \n* **api First**: the UI is just another client of the api. \n* **secured**: the services exposed need authentication for both humans or machines \n* **event based**: the services should expose a way to get notified of what happened inside. \n"},{"name":"api.md","id":"/api.md","url":"/api.html","title":"Admin REST API","content":"# Admin REST API\n\nOtoroshi provides a fully featured REST admin API to perform almost every operation possible in the Otoroshi dashboard. The Otoroshi dashbaord is just a regular consumer of the admin API.\n\nUsing the admin API, you can do whatever you want and enhance your Otoroshi instances with a lot of features that will feet your needs.\n\n## Swagger descriptor\n\nThe Otoroshi admin API is described using OpenAPI format and is available at :\n\nhttps://maif.github.io/otoroshi/manual/code/openapi.json\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/openapi.json\n\n## Swagger documentation\n\nYou can read the OpenAPI descriptor in a more human friendly fashion using `Swagger UI`. The swagger UI documentation of the Otoroshi admin API is available at :\n\nhttps://maif.github.io/otoroshi/swagger-ui/index.html\n\nEvery Otoroshi instance provides its own embedded OpenAPI descriptor at :\n\nhttp://otoroshi.oto.tools:8080/api/swagger/ui\n\nYou can also read the swagger UI documentation of the Otoroshi admin API below :\n\n@@@ div { .swagger-frame }\n\n\n@@@\n"},{"name":"architecture.md","id":"/architecture.md","url":"/architecture.html","title":"Architecture","content":"# Architecture\n\nWhen we started the development of Otoroshi, we had several classical patterns in mind like `Service gateway`, `Service locator`, `Circuit breakers`, etc ...\n\nAt start we thought about providing a bunch of librairies that would be included in each microservice or app to perform these tasks. But the more we were thinking about it, the more it was feeling weird, unagile, etc, it also prevented us to use any technical stack we wanted to use. So we decided to change our approach to something more universal.\n\nWe chose to make Otoroshi the central part of our microservices system, something between a reverse-proxy, a service gateway and a service locator where each call to a microservice (even from another microservice) must pass through Otoroshi. There are multiple benefits to do that, each call can be logged, audited, monitored, integrated with a circuit breaker, etc without imposing libraries and technical stack. Any service is exposed through its own domain and we rely only on DNS to handle the service location part. Any access to a service is secured by default with an api key and is supervised by a circuit breaker to avoid cascading failures.\n\n@@@ div { .centered-img }\n\n@@@\n\nOtoroshi tries to embrace our @ref:[global philosophy](./about.md#philosophy) by providing a full featured REST admin api, a gorgeous admin dashboard written in @link:[React](https://reactjs.org) { open=new } that uses the api, by generating traffic events, alerts events, audit events that can be consumed by several channels. Otoroshi also supports a bunch of datastores to better match with different use cases.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"aws.md","id":"/deploy/aws.md","url":"/deploy/aws.html","title":"AWS - Elastic Beanstalk","content":"# AWS - Elastic Beanstalk\n\nNow you want to use Otoroshi on AWS. There are multiple options to deploy Otoroshi on AWS, \nfor instance :\n\n* You can deploy the @ref:[Docker image](../install/get-otoroshi.md#from-docker) on [Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n* You can create a basic [Amazon EC2](https://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/concepts.html), access it via SSH, then \ndeploy the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file) \n* Or you can use [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk)\n\nIn this section we are going to cover how to deploy Otoroshi on [AWS Elastic Beanstalk](https://aws.amazon.com/fr/elasticbeanstalk). \n\n## AWS Elastic Beanstalk Overview\nUnlike Clever Cloud, to deploy an application on AWS Elastic Beanstalk, you don't link your app to your VCS repository, push your code and expect it to be built and run.\n\nAWS Elastic Beanstalk does only the run part. So you have to handle your own build pipeline, upload a Zip file containing your runnable, then AWS Elastic Beanstalk will take it from there. \n \nEg: for apps running on the JVM (Scala/Java/Kotlin) a Zip with the jar inside would suffice, for apps running in a Docker container, a Zip with the DockerFile would be enough. \n\n\n## Prepare your deployment target\nActually, there are 2 options to build your target. \n\nEither you create a DockerFile from this @ref:[Docker image](../install/get-otoroshi.md#from-docker), build a zip, and do all the Otoroshi custom configuration using ENVs.\n\nOr you download the @ref:[otoroshi.jar](../install/get-otoroshi.md#from-jar-file), do all the Otoroshi custom configuration using your own otoroshi.conf, and create a DockerFile that runs the jar using your otoroshi.conf. \n\nFor the second option your DockerFile would look like this :\n\n```dockerfile\nFROM openjdk:11\nVOLUME /tmp\nEXPOSE 8080\nADD otoroshi.jar otoroshi.jar\nADD otoroshi.conf otoroshi.conf\nRUN sh -c 'touch /otoroshi.jar'\nENV JAVA_OPTS=\"\"\nENTRYPOINT [ \"sh\", \"-c\", \"java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dconfig.file=/otoroshi.conf -jar /otoroshi.jar\" ]\n``` \n \nI'd recommend the second option.\n \nNow Zip your target (Jar + Conf + DockerFile) and get ready for deployment. \n\n## Create an Otoroshi instance on AWS Elastic Beanstalk\nFirst, go to [AWS Elastic Beanstalk Console](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/welcome), don't forget to sign in and make sure that you are in the good region (eg : eu-west-3 for Paris).\n\nHit **Get started** \n\n@@@ div { .centered-img }\n\n@@@\n\nSpecify the **Application name** of your application, Otoroshi for example.\n\n@@@ div { .centered-img }\n\n@@@\n \nChoose the **Platform** of the application you want to create, in your case use Docker.\n\nFor **Application code** choose **Upload your code** then hit **Upload**.\n\n@@@ div { .centered-img }\n\n@@@\n\nBrowse the zip created in the [previous section](#prepare-your-deployment-target) from your machine. \n\nAs you can see in the image above, you can also choose an S3 location, you can imagine that at the end of your build pipeline you upload your Zip to S3, and then get it from there (I wouldn't recommend that though).\n \nWhen the upload is done, hit **Configure more options**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nRight now an AWS Elastic Beanstalk application has been created, and by default an environment named Otoroshi-env is being created as well.\n\nAWS Elastic Beanstalk can manage multiple environments of the same application, for instance environments can be (prod, preprod, expriments...). \n\nOtoroshi is a bit particular, it doesn't make much sense to have multiple environments, since Otoroshi will handle all the requests from/to backend services regardless of the environment. \n \nAs you see in the image above, we are now configuring the Otoroshi-env, the one and only environment of Otoroshi.\n \nFor **Configuration presets**, choose custom configuration, now you have a load balancer for your environment with the capacity of at least one instance and at most four.\nI'd recommend at least 2 instances, to change that, on the **Capacity** card hit **Modify**. \n\n@@@ div { .centered-img }\n\n@@@\n\nChange the **Instances** to min 2, max 4 then hit **Save**. For the **Scaling triggers**, I'd keep the default values, but know that you can edit the capacity config any time you want, it only costs a redeploy, which will be done automatically by the way.\n \nInstances size is by default t2.micro, which is a bit small for running Otoroshi, I'd recommend a t2.medium. \nOn the **Instances** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFor **Instance type** choose t2.medium, then hit **Save**, no need to change the volume size, unless you have a lot of http call faults, which means a lot more logs, in that case the default volume size may not be enough.\n\nThe default environment created for Otoroshi, for instance Otoroshi-env, is a web server environment which fits in your case, but the thing is that on AWS Elastic Beanstalk by default a web server environment for a docker-based application, runs behind an Nginx proxy.\nWe have to remove that proxy. So on the **Software** card hit **Modify**.\n \n@@@ div { .centered-img }\n\n@@@ \n \nFor **Proxy server** choose None then hit **Save**.\n\nAlso note that you can set Envs for Otoroshi in same page (see image below). \n\n@@@ div { .centered-img }\n\n@@@ \n\nTo finalise the creation process, hit **Create app** on the bottom right.\n\nThe Otoroshi app is now created, and it's running which is cool, but we still don't have neither a **datastore** nor **https**.\n \n## Create an Otoroshi datastore on AWS ElastiCache\n\nBy default Otoroshi uses non persistent memory to store it's data, Otoroshi supports many kinds of datastores. In this section we will be covering Redis datastore. \n\nBefore starting, using a datastore hosted by AWS is not at all mandatory, feel free to use your own if you like, but if you want to learn more about ElastiCache, this section may interest you, otherwise you can skip it.\n\nGo to [AWS ElastiCache](https://eu-west-3.console.aws.amazon.com/elasticache/home?region=eu-west-3#) and hit **Get Started Now**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nFor **Cluster engine** keep Redis.\n\nChoose a **Name** for your datastore, for instance otoroshi-datastore.\n\nYou can keep all the other default values and hit **Create** on the bottom right of the page.\n\nOnce your Redis Cluster is created, it would look like the image below.\n\n@@@ div { .centered-img }\n\n@@@ \n\n\nFor applications in the same security group as your cluster, redis cluster is accessible via the **Primary Endpoint**. Don't worry the default security group is fine, you don't need any configuration to access the cluster from Otoroshi.\n\nTo make Otoroshi use the created cluster, you can either use Envs `APP_STORAGE=redis`, `REDIS_HOST` and `REDIS_PORT`, or set `otoroshi.storage=redis`, `otoroshi.redis.host` and `otoroshi.redis.port` in your otoroshi.conf.\n\n## Create SSL certificate and configure your domain\n\nOtoroshi has now a datastore, but not yet ready for use. \n\nIn order to get it ready you need to :\n\n* Configure Otoroshi with your domain \n* Create a wildcard SSL certificate for your domain\n* Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n* Configure your DNS to redirect all traffic on your domain to Otoroshi \n \n### Configure Otoroshi with your domain\n\nYou can use ENVs or you can use a custom otoroshi.conf in your Docker container.\n\nFor the second option your otoroshi.conf would look like this :\n\n``` \n include \"application.conf\"\n http.port = 8080\n app {\n env = \"prod\"\n domain = \"mysubdomain.oto.tools\"\n rootScheme = \"https\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n \n storage = \"redis\"\n redis {\n host=\"myredishost\"\n port=myredisport\n }\n \n privateapps {\n subdomain = \"privateapps\"\n }\n \n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-client-id\"\n backOfficeApiKeyClientSecret = \"admin-client-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n proxy {\n https = true\n local = false\n }\n }\n claim {\n sharedKey = \"myclaimsharedkey\"\n }\n }\n \n play.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2147483646\n domain = \".mysubdomain.oto.tools\"\n cookieName = \"oto-sess\"\n }\n }\n``` \n\n### Create a wildcard SSL certificate for your domain\n\nGo to [AWS Certificate Manager](https://eu-west-3.console.aws.amazon.com/acm/home?region=eu-west-3#/firstrun).\n\nBelow **Provision certificates** hit **Get started**.\n\n@@@ div { .centered-img }\n\n@@@ \n \nKeep the default selected value **Request a public certificate** and hit **Request a certificate**.\n \n@@@ div { .centered-img }\n\n@@@ \n\nPut your **Domain name**, use *. for wildcard, for instance *\\*.mysubdomain.oto.tools*, then hit **Next**.\n\n@@@ div { .centered-img }\n\n@@@ \n\nYou can choose between **Email validation** and **DNS validation**, I'd recommend **DNS validation**, then hit **Review**. \n \n@@@ div { .centered-img }\n\n@@@ \n \nVerify that you did put the right **Domain name** then hit **Confirm and request**. \n\n@@@ div { .centered-img }\n\n@@@\n \nAs you see in the image above, to let Amazon do the validation you have to add the `CNAME` record to your DNS configuration. Normally this operation takes around one day.\n \n### Configure Otoroshi AWS Elastic Beanstalk instance with the SSL certificate \n\nOnce the certificate is validated, you need to modify the configuration of Otoroshi-env to add the SSL certificate for HTTPS. \nFor that you need to go to [AWS Elastic Beanstalk applications](https://eu-west-3.console.aws.amazon.com/elasticbeanstalk/home?region=eu-west-3#/applications),\nhit **Otoroshi-env**, then on the left side hit **Configuration**, then on the **Load balancer** card hit **Modify**.\n\n@@@ div { .centered-img }\n\n@@@\n\nIn the **Application Load Balancer** section hit **Add listener**.\n\n@@@ div { .centered-img }\n\n@@@\n\nFill the popup as the image above, then hit **Add**. \n\nYou should now be seeing something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n \n \nMake sure that your listener is enabled, and on the bottom right of the page hit **Apply**.\n\nNow you have **https**, so let's use Otoroshi.\n\n### Configure your DNS to redirect all traffic on your domain to Otoroshi\n \nIt's actually pretty simple, you just need to add a `CNAME` record to your DNS configuration, that redirects *\\*.mysubdomain.oto.tools* to the DNS name of Otoroshi's load balancer.\n\nTo find the DNS name of Otoroshi's load balancer go to [AWS Ec2](https://eu-west-3.console.aws.amazon.com/ec2/v2/home?region=eu-west-3#LoadBalancers:tag:elasticbeanstalk:environment-name=Otoroshi-env;sort=loadBalancerName)\n\nYou would find something like this : \n \n@@@ div { .centered-img }\n\n@@@ \n\nThere is your DNS name, so add your `CNAME` record. \n \nOnce all these steps are done, the AWS Elastic Beanstalk Otoroshi instance, would now be handling all the requests on your domain. ;) \n"},{"name":"clever-cloud.md","id":"/deploy/clever-cloud.md","url":"/deploy/clever-cloud.html","title":"Clever-Cloud","content":"# Clever-Cloud\n\nNow you want to use Otoroshi on Clever Cloud. Otoroshi has been designed and created to run on Clever Cloud and a lot of choices were made because of how Clever Cloud works.\n\n## Create an Otoroshi instance on CleverCloud\n\nIf you want to customize the configuration @ref:[use env. variables](../install/setup-otoroshi.md#configuration-with-env-variables), you can use [the example provided below](#example-of-clevercloud-env-variables)\n\nCreate a new CleverCloud app based on a clevercloud git repo (not empty) or a github project of your own (not empty).\n\n@@@ div { .centered-img }\n\n@@@\n\nThen choose what kind of app your want to create, for Otoroshi, choose `Java + Jar`\n\n@@@ div { .centered-img }\n\n@@@\n\nNext, set up choose instance size and auto-scalling. Otoroshi can run on small instances, especially if you just want to test it.\n\n@@@ div { .centered-img }\n\n@@@\n\nFinally, choose a name for your app\n\n@@@ div { .centered-img }\n\n@@@\n\nNow you just need to customize environnment variables\n\nat this point, you can also add other env. variables to configure Otoroshi like in [the example provided below](#example-of-clevercloud-env-variables)\n\n@@@ div { .centered-img }\n\n@@@\n\nYou can also use expert mode :\n\n@@@ div { .centered-img }\n\n@@@\n\nNow, your app is ready, don't forget to add a custom domains name on the CleverCloud app matching the Otoroshi app domain. \n\n## Example of CleverCloud env. variables\n\nYou can add more env variables to customize your Otoroshi instance like the following. Use the expert mode to copy/paste all the values in one shot. If you want an real datastore, create a redis addon on clevercloud, link it to your otoroshi app and change the `APP_STORAGE` variable to `redis`\n\n
      \n\n
      \n```\nADMIN_API_CLIENT_ID=xxxx\nADMIN_API_CLIENT_SECRET=xxxxx\nADMIN_API_GROUP=xxxxxx\nADMIN_API_SERVICE_ID=xxxxxxx\nCLAIM_SHAREDKEY=xxxxxxx\nOTOROSHI_INITIAL_ADMIN_LOGIN=youremailaddress\nOTOROSHI_INITIAL_ADMIN_PASSWORD=yourpassword\nPLAY_CRYPTO_SECRET=xxxxxx\nSESSION_NAME=oto-session\nAPP_DOMAIN=yourdomain.tech\nAPP_ENV=prod\nAPP_STORAGE=inmemory\nAPP_ROOT_SCHEME=https\nCC_PRE_BUILD_HOOK=curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/${latest_otoroshi_version}/otoroshi.jar'\nCC_JAR_PATH=./otoroshi.jar\nCC_JAVA_VERSION=11\nPORT=8080\nSESSION_DOMAIN=.yourdomain.tech\nSESSION_MAX_AGE=604800000\nSESSION_SECURE_ONLY=true\nUSER_AGENT=otoroshi\nMAX_EVENTS_SIZE=1\nWEBHOOK_SIZE=100\nAPP_BACKOFFICE_SESSION_EXP=86400000\nAPP_PRIVATEAPPS_SESSION_EXP=86400000\nENABLE_METRICS=true\nOTOROSHI_ANALYTICS_PRESSURE_ENABLED=true\nUSE_CACHE=true\n```\n
      "},{"name":"clustering.md","id":"/deploy/clustering.md","url":"/deploy/clustering.html","title":"Otoroshi clustering","content":"# Otoroshi clustering\n\nOtoroshi can work as a cluster by default as you can spin many Otoroshi servers using the same datastore or datastore cluster. In that case any instance is capable of serving services, Otoroshi admin UI, Otoroshi admin API, etc.\n\nBut sometimes, this is not enough. So Otoroshi provides an additional clustering model named `Leader / Workers` where there is a leader cluster ([control plane](https://en.wikipedia.org/wiki/Control_plane)), composed of Otoroshi instances backed by a datastore like Redis, PostgreSQL or Cassandra, that is in charge of all `writes` to the datastore through Otoroshi admin UI and API, and a worker cluster ([data plane](https://en.wikipedia.org/wiki/Forwarding_plane)) composed of horizontally scalable Otoroshi instances, backed by a super fast in memory datastore, with the sole purpose of routing traffic to your services based on data synced from the leader cluster. With this distributed Otoroshi version, you can reach your goals of high availability, scalability and security.\n\nOtoroshi clustering only uses http internally (right now) to make communications between leaders and workers instances so it is fully compatible with PaaS providers like [Clever-Cloud](https://www.clever-cloud.com/en/) that only provide one external port for http traffic.\n\n@@@ div { .centered-img }\n\n\n*Fig. 1: Simplified view*\n@@@\n\n@@@ div { .centered-img }\n\n\n*Fig. 2: Deployment view*\n@@@\n\n## Cluster configuration\n\n```hocon\notoroshi {\n cluster {\n mode = \"leader\" # can be \"off\", \"leader\", \"worker\"\n compression = 4 # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9\n leader {\n name = ${?CLUSTER_LEADER_NAME} # name of the instance, if none, it will be generated\n urls = [\"http://127.0.0.1:8080\"] # urls to contact the leader cluster\n host = \"otoroshi-api.oto.tools\" # host of the otoroshi api in the leader cluster\n clientId = \"apikey-id\" # otoroshi api client id\n clientSecret = \"secret\" # otoroshi api client secret\n cacheStateFor = 4000 # state is cached during (ms)\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME} # name of the instance, if none, it will be generated\n retries = 3 # number of retries when calling leader cluster\n timeout = 2000 # timeout when calling leader cluster\n state {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on state sync\n pollEvery = 10000 # interval of time (ms) between 2 state sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on state sync\n }\n quotas {\n retries = ${otoroshi.cluster.worker.retries} # number of retries when calling leader cluster on quotas sync\n pushEvery = 2000 # interval of time (ms) between 2 quotas sync\n timeout = ${otoroshi.cluster.worker.timeout} # timeout when calling leader cluster on quotas sync\n }\n }\n }\n}\n```\n\nyou can also use many env. variables to configure Otoroshi cluster\n\n```hocon\notoroshi {\n cluster {\n mode = ${?CLUSTER_MODE}\n compression = ${?CLUSTER_COMPRESSION}\n leader {\n name = ${?CLUSTER_LEADER_NAME}\n host = ${?CLUSTER_LEADER_HOST}\n url = ${?CLUSTER_LEADER_URL}\n clientId = ${?CLUSTER_LEADER_CLIENT_ID}\n clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET}\n groupingBy = ${?CLUSTER_LEADER_GROUP_BY}\n cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR}\n stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH}\n }\n worker {\n name = ${?CLUSTER_WORKER_NAME}\n retries = ${?CLUSTER_WORKER_RETRIES}\n timeout = ${?CLUSTER_WORKER_TIMEOUT}\n state {\n retries = ${?CLUSTER_WORKER_STATE_RETRIES}\n pollEvery = ${?CLUSTER_WORKER_POLL_EVERY}\n timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT}\n }\n quotas {\n retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES}\n pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY}\n timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT}\n }\n }\n }\n}\n```\n\n@@@ warning\nYou **should** use HTTPS exposition for the Otoroshi API that will be used for data sync as sensitive informations are exchanged between control plane and data plane.\n@@@\n\n@@@ warning\nYou **must** have the same cluster configuration on every Otoroshi instance (worker/leader) with only names and mode changed for each instance. Some things in leader/worker are computed using configuration of their counterpart worker/leader.\n@@@\n\n## Cluster UI\n\nOnce an Otoroshi instance is launcher as cluster Leader, a new row of live metrics tile will be available on the home page of Otoroshi admin UI.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can also access a more detailed view of the cluster at `Settings (cog icon) / Cluster View`\n\n@@@ div { .centered-img }\n\n@@@\n\n## Run examples\n\nfor leader \n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nfor worker\n\n```sh\njava -Dhttp.port=8092 -Dhttps.port=9092 -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0=http://127.0.0.1:8091 -jar otoroshi.jar\n```\n\n## Setup a cluster by example\n\nif you want to see how to setup an otoroshi cluster, just check @ref:[the clustering tutorial](../how-to-s/setup-otoroshi-cluster.md)"},{"name":"index.md","id":"/deploy/index.md","url":"/deploy/index.html","title":"Deploy to production","content":"# Deploy to production\n\nNow it's time to deploy Otoroshi in production, in this chapter we will see what kind of things you can do.\n\nOtoroshi can run wherever you want, even on a raspberry pi (Cluster^^) ;)\n\n@@@div { .plugin .platform }\n\n## Cloud APIM\n\nCloud APIM provides Otoroshi instances as a service. You can easily create production ready Otoroshi clusters in just a few clics.\n\n\n[Documentation](https://www.cloud-apim.com/)\n@@@\n\n@@@div { .plugin .platform }\n\n## Clever Cloud\n\nOtoroshi provides an integration to create easily services based on application deployed on your Clever Cloud account.\n\n\n@ref:[Documentation](./clever-cloud.md)\n@@@\n\n@@@div { .plugin .platform } \n## Kubernetes\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support.\n\n\n\n@ref:[Documentation](./kubernetes.md)\n@@@\n\n@@@div { .plugin .platform } \n## AWS Elastic Beanstalk\n\nRun Otoroshi on AWS Elastic Beanstalk\n\n\n\n@ref:[Tutorial](./aws.md)\n@@@\n\n@@@div { .plugin .platform } \n## Amazon ECS\n\nDeploy the Otoroshi Docker image using Amazon Elastic Container Service\n\n\n\n@link:[Tutorial](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform }\n## GCE\n\nDeploy the Docker image using Google Compute Engine container integration\n\n\n\n@link:[Documentation](https://cloud.google.com/compute/docs/containers/deploying-containers)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n\n@@@\n\n@@@div { .plugin .platform } \n## Azure\n\nDeploy the Docker image using Azure Container Service\n\n\n\n@link:[Documentation](https://azure.microsoft.com/en-us/services/container-service/)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker) \n@@@\n\n@@@div { .plugin .platform } \n## Heroku\n\nDeploy the Docker image using Docker integration\n\n\n\n@link:[Documentation](https://devcenter.heroku.com/articles/container-registry-and-runtime)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform } \n## CloudFoundry\n\nDeploy the Docker image using -Docker integration\n\n\n\n@link:[Documentation](https://docs.cloudfoundry.org/adminguide/docker.html)\n@ref:[Docker image](../install/get-otoroshi.md#from-docker)\n@@@\n\n@@@div { .plugin .platform .platform-actions-column } \n## Your own infrastructure\n\nAs Otoroshi is a Play Framework application, you can read the doc about putting a `Play` app in production.\n\nDownload the latest Otoroshi distribution, unzip it, customize it and run it.\n\n@link:[Play Framework](https://www.playframework.com)\n@link:[Production Configuration](https://www.playframework.com/documentation/2.6.x/ProductionConfiguration)\n@ref:[Otoroshi distribution](../install/get-otoroshi.md#from-zip)\n@@@\n\n@@@div { .break }\n## Scaling and clustering in production\n@@@\n\n\n@@@div { .plugin .platform .dark-platform } \n## Clustering\n\nDeploy Otoroshi as a cluster of leaders and workers.\n\n\n@ref:[Documentation](./clustering.md)\n@@@\n\n@@@div { .plugin .platform .dark-platform } \n## Scaling Otoroshi\n\nOtoroshi is designed to be reasonably easy to scale and be highly available.\n\n\n@ref:[Documentation](./scaling.md) \n@@@\n\n@@@ index\n\n* [Clustering](./clustering.md)\n* [Kubernetes](./kubernetes.md)\n* [Clever Cloud](./clever-cloud.md)\n* [AWS - Elastic Beanstalk](./aws.md)\n* [Scaling](./scaling.md) \n\n@@@\n"},{"name":"kubernetes.md","id":"/deploy/kubernetes.md","url":"/deploy/kubernetes.html","title":"Kubernetes","content":"# Kubernetes\n\nStarting at version 1.5.0, Otoroshi provides a native Kubernetes support. Multiple otoroshi jobs (that are actually kubernetes controllers) are provided in order to\n\n- sync kubernetes secrets of type `kubernetes.io/tls` to otoroshi certificates\n- act as a standard ingress controller (supporting `Ingress` objects)\n- provide Custom Resource Definitions (CRDs) to manage Otoroshi entities from Kubernetes and act as an ingress controller with its own resources\n\n## Installing otoroshi on your kubernetes cluster\n\n@@@ warning\nYou need to have cluster admin privileges to install otoroshi and its service account, role mapping and CRDs on a kubernetes cluster. We also advise you to create a dedicated namespace (you can name it `otoroshi` for example) to install otoroshi\n@@@\n\nIf you want to deploy otoroshi into your kubernetes cluster, you can download the deployment descriptors from https://github.com/MAIF/otoroshi/tree/master/kubernetes and use kustomize to create your own overlay.\n\nYou can also create a `kustomization.yaml` file with a remote base\n\n```yaml\nbases:\n- github.com/MAIF/otoroshi/kubernetes/kustomize/overlays/simple/?ref=v16.15.0-dev\n```\n\nThen deploy it with `kubectl apply -k ./overlays/myoverlay`. \n\nYou can also use Helm to deploy a simple otoroshi cluster on your kubernetes cluster\n\n```sh\nhelm repo add otoroshi https://maif.github.io/otoroshi/helm\nhelm install my-otoroshi otoroshi/otoroshi\n```\n\nBelow, you will find example of deployment. Do not hesitate to adapt them to your needs. Those descriptors have value placeholders that you will need to replace with actual values like \n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: ${domain}\n```\n\nyou will have to edit it to make it look like\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'apis.my.domain'\n```\n\nif you don't want to use placeholders and environment variables, you can create a secret containing the configuration file of otoroshi\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: otoroshi-config\ntype: Opaque\nstringData:\n oto.conf: >\n include \"application.conf\"\n app {\n storage = \"redis\"\n domain = \"apis.my.domain\"\n }\n```\n\nand mount it in the otoroshi container\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: otoroshi-deployment\nspec:\n selector:\n matchLabels:\n run: otoroshi-deployment\n template:\n metadata:\n labels:\n run: otoroshi-deployment\n spec:\n serviceAccountName: otoroshi-admin-user\n terminationGracePeriodSeconds: 60\n hostNetwork: false\n containers:\n - image: maif/otoroshi:16.15.0-dev\n imagePullPolicy: IfNotPresent\n name: otoroshi\n args: ['-Dconfig.file=/usr/app/otoroshi/conf/oto.conf']\n ports:\n - containerPort: 8080\n name: \"http\"\n protocol: TCP\n - containerPort: 8443\n name: \"https\"\n protocol: TCP\n volumeMounts:\n - name: otoroshi-config\n mountPath: \"/usr/app/otoroshi/conf\"\n readOnly: true\n volumes:\n - name: otoroshi-config\n secret:\n secretName: otoroshi-config\n ...\n```\n\nYou can also create several secrets for each placeholder, mount them to the otoroshi container then use their file path as value\n\n```yaml\n env:\n - name: APP_STORAGE_ROOT\n value: otoroshi\n - name: APP_DOMAIN\n value: 'file:///the/path/of/the/secret/file'\n```\n\nyou can use the same trick in the config. file itself\n\n### Note on bare metal kubernetes cluster installation\n\n@@@ note\nBare metal kubernetes clusters don't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples below.\n@@@\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@\n\n### Common manifests\n\nthe following manifests are always needed. They create otoroshi CRDs, tokens, role, etc. Redis deployment is not mandatory, it's just an example. You can use your own existing setup.\n\nrbac.yaml\n: @@snip [rbac.yaml](../snippets/kubernetes/kustomize/base/rbac.yaml) \n\ncrds.yaml\n: @@snip [crds.yaml](../snippets/kubernetes/kustomize/base/crds.yaml) \n\nredis.yaml\n: @@snip [redis.yaml](../snippets/kubernetes/kustomize/base/redis.yaml) \n\n\n### Deploy a simple otoroshi instanciation on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. We use a service of type `LoadBalancer` to expose otoroshi to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple/dns.example) \n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster\n\nHere we have 2 replicas connected to the same redis instance. Nothing fancy. The otoroshi instance are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal/dns.example) \n\n\n### Deploy a simple otoroshi instanciation on a bare metal kubernetes cluster using a DaemonSet\n\nHere we have one otoroshi instance on each kubernetes node (with the `otoroshi-kind: instance` label) with redis persistance. The otoroshi instances are exposed as `hostPort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/deployment.yaml) \n\nhaproxy.example\n: @@snip [haproxy.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/haproxy.example) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/simple-baremetal-daemonset/dns.example) \n\n### Deploy an otoroshi cluster on a cloud provider managed kubernetes cluster\n\nHere we have 2 replicas of an otoroshi leader connected to a redis instance and 2 replicas of an otoroshi worker connected to the leader. We use a service of type `LoadBalancer` to expose otoroshi leader/worker to the rest of the world. You have to setup your DNS to bind otoroshi domain names to the `LoadBalancer` external `CNAME` (see the example below)\n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster/deployment.yaml) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster\n\nHere we have 2 replicas of otoroshi leader connected to the same redis instance and 2 replicas for otoroshi worker. The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal/dns.example) \n\n### Deploy an otoroshi cluster on a bare metal kubernetes cluster using DaemonSet\n\nHere we have 1 otoroshi leader instance on each kubernetes node (with the `otoroshi-kind: leader` label) connected to the same redis instance and 1 otoroshi worker instance on each kubernetes node (with the `otoroshi-kind: worker` label). The otoroshi instances are exposed as `nodePort` so you'll have to add a loadbalancer in front of your kubernetes nodes to route external traffic (TCP) to your otoroshi instances. You have to setup your DNS to bind otoroshi domain names to your loadbalancer (see the example below). \n\ndeployment.yaml\n: @@snip [deployment.yaml](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/deployment.yaml) \n\nnginx.example\n: @@snip [nginx.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/nginx.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\ndns.example\n: @@snip [dns.example](../snippets/kubernetes/kustomize/overlays/cluster-baremetal-daemonset/dns.example) \n\n## Using Otoroshi as an Ingress Controller\n\nIf you want to use Otoroshi as an [Ingress Controller](https://kubernetes.io/fr/docs/concepts/services-networking/ingress/), just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Ingress Controller`.\n\nThen add the following configuration for the job (with your own tweaks of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": true, // sync ingresses\n \"crds\": false, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {},\n \"routes\": {},\n \"route-compositions\": {},\n \"backends\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nNow you can deploy your first service ;)\n\n### Deploy an ingress route\n\nnow let's say you want to deploy an http service and route to the outside world through otoroshi\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: kennethreitz/httpbin\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 80\n name: \"http\"\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8080\n targetPort: http\n name: http\n selector:\n run: http-app-deployment\n---\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nonce deployed, otoroshi will sync with kubernetes and create the corresponding service to route your app. You will be able to access your app with\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get\n```\n\n### Support for Ingress Classes\n\nSince Kubernetes 1.18, you can use `IngressClass` type of manifest to specify which ingress controller you want to use for a deployment (https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#extended-configuration-with-ingress-classes). Otoroshi is fully compatible with this new manifest `kind`. To use it, configure the Ingress job to match your controller\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClasses\": [\"otoroshi.io/ingress-controller\"],\n ...\n }\n}\n```\n\nthen you have to deploy an `IngressClass` to declare Otoroshi as an ingress controller\n\n```yaml\napiVersion: \"networking.k8s.io/v1beta1\"\nkind: \"IngressClass\"\nmetadata:\n name: \"otoroshi-ingress-controller\"\nspec:\n controller: \"otoroshi.io/ingress-controller\"\n parameters:\n apiGroup: \"proxy.otoroshi.io/v1alpha\"\n kind: \"IngressParameters\"\n name: \"otoroshi-ingress-controller\"\n```\n\nand use it in your `Ingress`\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\nspec:\n ingressClassName: otoroshi-ingress-controller\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\n### Use multiple ingress controllers\n\nIt is of course possible to use multiple ingress controller at the same time (https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers) using the annotation `kubernetes.io/ingress.class`. By default, otoroshi reacts to the class `otoroshi`, but you can make it the default ingress controller with the following config\n\n```json\n{\n \"KubernetesConfig\": {\n ...\n \"ingressClass\": \"*\",\n ...\n }\n}\n```\n\n### Supported annotations\n\nif you need to customize the service descriptor behind an ingress rule, you can use some annotations. If you need better customisation, just go to the CRDs part. The following annotations are supported :\n\n- `ingress.otoroshi.io/groups`\n- `ingress.otoroshi.io/group`\n- `ingress.otoroshi.io/groupId`\n- `ingress.otoroshi.io/name`\n- `ingress.otoroshi.io/targetsLoadBalancing`\n- `ingress.otoroshi.io/stripPath`\n- `ingress.otoroshi.io/enabled`\n- `ingress.otoroshi.io/userFacing`\n- `ingress.otoroshi.io/privateApp`\n- `ingress.otoroshi.io/forceHttps`\n- `ingress.otoroshi.io/maintenanceMode`\n- `ingress.otoroshi.io/buildMode`\n- `ingress.otoroshi.io/strictlyPrivate`\n- `ingress.otoroshi.io/sendOtoroshiHeadersBack`\n- `ingress.otoroshi.io/readOnly`\n- `ingress.otoroshi.io/xForwardedHeaders`\n- `ingress.otoroshi.io/overrideHost`\n- `ingress.otoroshi.io/allowHttp10`\n- `ingress.otoroshi.io/logAnalyticsOnServer`\n- `ingress.otoroshi.io/useAkkaHttpClient`\n- `ingress.otoroshi.io/useNewWSClient`\n- `ingress.otoroshi.io/tcpUdpTunneling`\n- `ingress.otoroshi.io/detectApiKeySooner`\n- `ingress.otoroshi.io/letsEncrypt`\n- `ingress.otoroshi.io/publicPatterns`\n- `ingress.otoroshi.io/privatePatterns`\n- `ingress.otoroshi.io/additionalHeaders`\n- `ingress.otoroshi.io/additionalHeadersOut`\n- `ingress.otoroshi.io/missingOnlyHeadersIn`\n- `ingress.otoroshi.io/missingOnlyHeadersOut`\n- `ingress.otoroshi.io/removeHeadersIn`\n- `ingress.otoroshi.io/removeHeadersOut`\n- `ingress.otoroshi.io/headersVerification`\n- `ingress.otoroshi.io/matchingHeaders`\n- `ingress.otoroshi.io/ipFiltering.whitelist`\n- `ingress.otoroshi.io/ipFiltering.blacklist`\n- `ingress.otoroshi.io/api.exposeApi`\n- `ingress.otoroshi.io/api.openApiDescriptorUrl`\n- `ingress.otoroshi.io/healthCheck.enabled`\n- `ingress.otoroshi.io/healthCheck.url`\n- `ingress.otoroshi.io/jwtVerifier.ids`\n- `ingress.otoroshi.io/jwtVerifier.enabled`\n- `ingress.otoroshi.io/jwtVerifier.excludedPatterns`\n- `ingress.otoroshi.io/authConfigRef`\n- `ingress.otoroshi.io/redirection.enabled`\n- `ingress.otoroshi.io/redirection.code`\n- `ingress.otoroshi.io/redirection.to`\n- `ingress.otoroshi.io/clientValidatorRef`\n- `ingress.otoroshi.io/transformerRefs`\n- `ingress.otoroshi.io/transformerConfig`\n- `ingress.otoroshi.io/accessValidator.enabled`\n- `ingress.otoroshi.io/accessValidator.excludedPatterns`\n- `ingress.otoroshi.io/accessValidator.refs`\n- `ingress.otoroshi.io/accessValidator.config`\n- `ingress.otoroshi.io/preRouting.enabled`\n- `ingress.otoroshi.io/preRouting.excludedPatterns`\n- `ingress.otoroshi.io/preRouting.refs`\n- `ingress.otoroshi.io/preRouting.config`\n- `ingress.otoroshi.io/issueCert`\n- `ingress.otoroshi.io/issueCertCA`\n- `ingress.otoroshi.io/gzip.enabled`\n- `ingress.otoroshi.io/gzip.excludedPatterns`\n- `ingress.otoroshi.io/gzip.whiteList`\n- `ingress.otoroshi.io/gzip.blackList`\n- `ingress.otoroshi.io/gzip.bufferSize`\n- `ingress.otoroshi.io/gzip.chunkedThreshold`\n- `ingress.otoroshi.io/gzip.compressionLevel`\n- `ingress.otoroshi.io/cors.enabled`\n- `ingress.otoroshi.io/cors.allowOrigin`\n- `ingress.otoroshi.io/cors.exposeHeaders`\n- `ingress.otoroshi.io/cors.allowHeaders`\n- `ingress.otoroshi.io/cors.allowMethods`\n- `ingress.otoroshi.io/cors.excludedPatterns`\n- `ingress.otoroshi.io/cors.maxAge`\n- `ingress.otoroshi.io/cors.allowCredentials`\n- `ingress.otoroshi.io/clientConfig.useCircuitBreaker`\n- `ingress.otoroshi.io/clientConfig.retries`\n- `ingress.otoroshi.io/clientConfig.maxErrors`\n- `ingress.otoroshi.io/clientConfig.retryInitialDelay`\n- `ingress.otoroshi.io/clientConfig.backoffFactor`\n- `ingress.otoroshi.io/clientConfig.connectionTimeout`\n- `ingress.otoroshi.io/clientConfig.idleTimeout`\n- `ingress.otoroshi.io/clientConfig.callAndStreamTimeout`\n- `ingress.otoroshi.io/clientConfig.callTimeout`\n- `ingress.otoroshi.io/clientConfig.globalTimeout`\n- `ingress.otoroshi.io/clientConfig.sampleInterval`\n- `ingress.otoroshi.io/enforceSecureCommunication`\n- `ingress.otoroshi.io/sendInfoToken`\n- `ingress.otoroshi.io/sendStateChallenge`\n- `ingress.otoroshi.io/secComHeaders.claimRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateRequestName`\n- `ingress.otoroshi.io/secComHeaders.stateResponseName`\n- `ingress.otoroshi.io/secComTtl`\n- `ingress.otoroshi.io/secComVersion`\n- `ingress.otoroshi.io/secComInfoTokenVersion`\n- `ingress.otoroshi.io/secComExcludedPatterns`\n- `ingress.otoroshi.io/secComSettings.size`\n- `ingress.otoroshi.io/secComSettings.secret`\n- `ingress.otoroshi.io/secComSettings.base64`\n- `ingress.otoroshi.io/secComUseSameAlgo`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.size`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeOtoToBack.base64`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.size`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.secret`\n- `ingress.otoroshi.io/secComAlgoChallengeBackToOto.base64`\n- `ingress.otoroshi.io/secComAlgoInfoToken.size`\n- `ingress.otoroshi.io/secComAlgoInfoToken.secret`\n- `ingress.otoroshi.io/secComAlgoInfoToken.base64`\n- `ingress.otoroshi.io/securityExcludedPatterns`\n\nfor more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n\nwith the previous example, the ingress does not define any apikey, so the route is public. If you want to enable apikeys on it, you can deploy the following descriptor\n\n```yaml\napiVersion: networking.k8s.io/v1beta1\nkind: Ingress\nmetadata:\n name: http-app-ingress\n annotations:\n kubernetes.io/ingress.class: otoroshi\n ingress.otoroshi.io/group: http-app-group\n ingress.otoroshi.io/forceHttps: 'true'\n ingress.otoroshi.io/sendOtoroshiHeadersBack: 'true'\n ingress.otoroshi.io/overrideHost: 'true'\n ingress.otoroshi.io/allowHttp10: 'false'\n ingress.otoroshi.io/publicPatterns: ''\nspec:\n tls:\n - hosts:\n - httpapp.foo.bar\n secretName: http-app-cert\n rules:\n - host: httpapp.foo.bar\n http:\n paths:\n - path: /\n backend:\n serviceName: http-app-service\n servicePort: 8080\n```\n\nnow you can use an existing apikey in the `http-app-group` to access your app\n\n```sh\ncurl -X GET https://httpapp.foo.bar/get -u existing-apikey-1:secret-1\n```\n\n## Use Otoroshi CRDs for a better/full integration\n\nOtoroshi provides some Custom Resource Definitions for kubernetes in order to manage Otoroshi related entities in kubernetes\n\n- `routes`\n- `backends`\n- `route-compositions`\n- `service-descriptors`\n- `tcp-services`\n- `error-templates`\n- `apikeys`\n- `certificates`\n- `jwt-verifiers`\n- `auth-modules`\n- `admin-sessions`\n- `admins`\n- `auth-module-users`\n- `service-groups`\n- `organizations`\n- `tenants`\n- `teams`\n- `data-exporters`\n- `scripts`\n- `wasm-plugins`\n- `global-configs`\n- `green-scores`\n- `coraza-configs`\n\nusing CRDs, you will be able to deploy and manager those entities from kubectl or the kubernetes api like\n\n```sh\nsudo kubectl get apikeys --all-namespaces\nsudo kubectl get service-descriptors --all-namespaces\ncurl -X GET \\\n -H 'Authorization: Bearer eyJhbGciOiJSUzI....F463SrpOehQRaQ' \\\n -H 'Accept: application/json' -k \\\n https://127.0.0.1:6443/apis/proxy.otoroshi.io/v1/apikeys | jq\n```\n\nYou can see this as better `Ingress` resources. Like any `Ingress` resource can define which controller it uses (using the `kubernetes.io/ingress.class` annotation), you can chose another kind of resource instead of `Ingress`. With Otoroshi CRDs you can even define resources like `Certificate`, `Apikey`, `AuthModules`, `JwtVerifier`, etc. It will help you to use all the power of Otoroshi while using the deployment model of kubernetes.\n \n@@@ warning\nwhen using Otoroshi CRDs, Kubernetes becomes the single source of truth for the synced entities. It means that any value in the descriptors deployed will overrides the one in Otoroshi datastore each time it's synced. So be careful if you use the Otoroshi UI or the API, some changes in configuration may be overriden by CRDs sync job.\n@@@\n\n### Resources examples\n\ngroup.yaml\n: @@snip [group.yaml](../snippets/crds/group.yaml) \n\napikey.yaml\n: @@snip [apikey.yaml](../snippets/crds/apikey.yaml) \n\nservice-descriptor.yaml\n: @@snip [service.yaml](../snippets/crds/service-descriptor.yaml) \n\ncertificate.yaml\n: @@snip [cert.yaml](../snippets/crds/certificate.yaml) \n\njwt.yaml\n: @@snip [jwt.yaml](../snippets/crds/jwt.yaml) \n\nauth.yaml\n: @@snip [auth.yaml](../snippets/crds/auth.yaml) \n\norganization.yaml\n: @@snip [orga.yaml](../snippets/crds/organization.yaml) \n\nteam.yaml\n: @@snip [team.yaml](../snippets/crds/team.yaml) \n\n\n### Configuration\n\nTo configure it, just go to the danger zone, and in `Global scripts` add the job named `Kubernetes Otoroshi CRDs Controller`. Then add the following configuration for the job (with your own tweak of course)\n\n```json\n{\n \"KubernetesConfig\": {\n \"enabled\": true,\n \"crds\": true,\n \"endpoint\": \"https://127.0.0.1:6443\",\n \"token\": \"eyJhbGciOiJSUzI....F463SrpOehQRaQ\",\n \"namespaces\": [\n \"*\"\n ]\n }\n}\n```\n\nthe configuration can have the following values \n\n```javascript\n{\n \"KubernetesConfig\": {\n \"endpoint\": \"https://127.0.0.1:6443\", // the endpoint to talk to the kubernetes api, optional\n \"token\": \"xxxx\", // the bearer token to talk to the kubernetes api, optional\n \"userPassword\": \"user:password\", // the user password tuple to talk to the kubernetes api, optional\n \"caCert\": \"/etc/ca.cert\", // the ca cert file path to talk to the kubernetes api, optional\n \"trust\": false, // trust any cert to talk to the kubernetes api, optional\n \"namespaces\": [\"*\"], // the watched namespaces\n \"labels\": [\"label\"], // the watched namespaces\n \"ingressClasses\": [\"otoroshi\"], // the watched kubernetes.io/ingress.class annotations, can be *\n \"defaultGroup\": \"default\", // the group to put services in otoroshi\n \"ingresses\": false, // sync ingresses\n \"crds\": true, // sync crds\n \"kubeLeader\": false, // delegate leader election to kubernetes, to know where the sync job should run\n \"restartDependantDeployments\": true, // when a secret/cert changes from otoroshi sync, restart dependant deployments\n \"templates\": { // template for entities that will be merged with kubernetes entities. can be \"default\" to use otoroshi default templates\n \"service-group\": {},\n \"service-descriptor\": {},\n \"apikeys\": {},\n \"global-config\": {},\n \"jwt-verifier\": {},\n \"tcp-service\": {},\n \"certificate\": {},\n \"auth-module\": {},\n \"data-exporter\": {},\n \"script\": {},\n \"organization\": {},\n \"team\": {},\n \"data-exporter\": {}\n }\n }\n}\n```\n\nIf `endpoint` is not defined, Otoroshi will try to get it from `$KUBERNETES_SERVICE_HOST` and `$KUBERNETES_SERVICE_PORT`.\nIf `token` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/token`.\nIf `caCert` is not defined, Otoroshi will try to get it from the file at `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.\nIf `$KUBECONFIG` is defined, `endpoint`, `token` and `caCert` will be read from the current context of the file referenced by it.\n\nyou can find a more complete example of the configuration object [here](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/plugins/jobs/kubernetes/config.scala#L134-L163)\n\n### Note about `apikeys` and `certificates` resources\n\nApikeys and Certificates are a little bit different than the other resources. They have ability to be defined without their secret part, but with an export setting so otoroshi will generate the secret parts and export the apikey or the certificate to kubernetes secret. Then any app will be able to mount them as volumes (see the full example below)\n\nIn those resources you can define \n\n```yaml\nexportSecret: true \nsecretName: the-secret-name\n```\n\nand omit `clientSecret` for apikey or `publicKey`, `privateKey` for certificates. For certificate you will have to provide a `csr` for the certificate in order to generate it\n\n```yaml\ncsr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n - httpapps.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n```\n\nwhen apikeys are exported as kubernetes secrets, they will have the type `otoroshi.io/apikey-secret` with values `clientId` and `clientSecret`\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: apikey-1\ntype: otoroshi.io/apikey-secret\ndata:\n clientId: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n clientSecret: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n```\n\nwhen certificates are exported as kubernetes secrets, they will have the type `kubernetes.io/tls` with the standard values `tls.crt` (the full cert chain) and `tls.key` (the private key). For more convenience, they will also have a `cert.crt` value containing the actual certificate without the ca chain and `ca-chain.crt` containing the ca chain without the certificate.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: certificate-1\ntype: kubernetes.io/tls\ndata:\n tls.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n tls.key: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n cert.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA==\n ca-chain.crt: TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA== \n```\n\n## Full CRD example\n\nthen you can deploy the previous example with better configuration level, and using mtls, apikeys, etc\n\nLet say the app looks like :\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\n// here we read the apikey to access http-app-2 from files mounted from secrets\nconst clientId = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientId').toString('utf8')\nconst clientSecret = fs.readFileSync('/var/run/secrets/kubernetes.io/apikeys/clientSecret').toString('utf8')\n\nconst backendKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/tls.key').toString('utf8')\nconst backendCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/cert.crt').toString('utf8')\nconst backendCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/backend/ca-chain.crt').toString('utf8')\n\nconst clientKey = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/tls.key').toString('utf8')\nconst clientCert = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/cert.crt').toString('utf8')\nconst clientCa = fs.readFileSync('/var/run/secrets/kubernetes.io/certs/client/ca-chain.crt').toString('utf8')\n\nfunction callApi2() {\n return new Promise((success, failure) => {\n const options = { \n // using the implicit internal name (*.global.otoroshi.mesh) of the other service descriptor passing through otoroshi\n hostname: 'http-app-service-descriptor-2.global.otoroshi.mesh', \n port: 433, \n path: '/', \n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Otoroshi-Client-Id': clientId,\n 'Otoroshi-Client-Secret': clientSecret,\n },\n cert: clientCert,\n key: clientKey,\n ca: clientCa\n }; \n let data = '';\n const req = https.request(options, (res) => { \n res.on('data', (d) => { \n data = data + d.toString('utf8');\n }); \n res.on('end', () => { \n success({ body: JSON.parse(data), res });\n }); \n res.on('error', (e) => { \n failure(e);\n }); \n }); \n req.end();\n })\n}\n\nconst options = { \n key: backendKey, \n cert: backendCert, \n ca: backendCa, \n // we want mtls behavior\n requestCert: true, \n rejectUnauthorized: true\n}; \nhttps.createServer(options, (req, res) => { \n res.writeHead(200, {'Content-Type': 'application/json'});\n callApi2().then(resp => {\n res.write(JSON.stringify{ (\"message\": `Hello to ${req.socket.getPeerCertificate().subject.CN}`, api2: resp.body })); \n });\n}).listen(433);\n```\n\nthen, the descriptors will be :\n\n```yaml\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: http-app-deployment\nspec:\n selector:\n matchLabels:\n run: http-app-deployment\n replicas: 1\n template:\n metadata:\n labels:\n run: http-app-deployment\n spec:\n containers:\n - image: foo/http-app\n imagePullPolicy: IfNotPresent\n name: otoroshi\n ports:\n - containerPort: 443\n name: \"https\"\n volumeMounts:\n - name: apikey-volume\n # here you will be able to read apikey from files \n # - /var/run/secrets/kubernetes.io/apikeys/clientId\n # - /var/run/secrets/kubernetes.io/apikeys/clientSecret\n mountPath: \"/var/run/secrets/kubernetes.io/apikeys\"\n readOnly: true\n volumeMounts:\n - name: backend-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/backend/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/backend/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/backend\"\n readOnly: true\n - name: client-cert-volume\n # here you will be able to read app cert from files \n # - /var/run/secrets/kubernetes.io/certs/client/tls.crt\n # - /var/run/secrets/kubernetes.io/certs/client/tls.key\n mountPath: \"/var/run/secrets/kubernetes.io/certs/client\"\n readOnly: true\n volumes:\n - name: apikey-volume\n secret:\n # here we reference the secret name from apikey http-app-2-apikey-1\n secretName: secret-2\n - name: backend-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-backend\n secretName: http-app-certificate-backend-secret\n - name: client-cert-volume\n secret:\n # here we reference the secret name from cert http-app-certificate-client\n secretName: http-app-certificate-client-secret\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: http-app-service\nspec:\n ports:\n - port: 8443\n targetPort: https\n name: https\n selector:\n run: http-app-deployment\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceGroup\nmetadata:\n name: http-app-group\n annotations:\n otoroshi.io/id: http-app-group\nspec:\n description: a group to hold services about the http-app\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-apikey-1\n# this apikey can be used to access the app\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-1\n authorizedEntities: \n - group_http-app-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-1\n# this apikey can be used to access another app in a different group\nspec:\n # a secret name secret-1 will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: secret-2\n authorizedEntities: \n - group_http-app-2-group\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-frontend\nspec:\n description: certificate for the http-app on otorshi frontend\n autoRenew: true\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - httpapp.foo.bar\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-front, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-backend\nspec:\n description: certificate for the http-app deployed on pods\n autoRenew: true\n # a secret name http-app-certificate-backend-secret will be created by otoroshi and can be used by containers\n exportSecret: true \n secretName: http-app-certificate-backend-secret\n csr:\n issuer: CN=Otoroshi Root\n hosts: \n - http-app-service \n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-back, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: Certificate\nmetadata:\n name: http-app-certificate-client\nspec:\n description: certificate for the http-app\n autoRenew: true\n secretName: http-app-certificate-client-secret\n csr:\n issuer: CN=Otoroshi Root\n key:\n algo: rsa\n size: 2048\n subject: UID=httpapp-client, O=OtoroshiApps\n client: false\n ca: false\n duration: 31536000000\n signatureAlg: SHA256WithRSAEncryption\n digestAlg: SHA-256\n---\napiVersion: proxy.otoroshi.io/v1\nkind: ServiceDescriptor\nmetadata:\n name: http-app-service-descriptor\nspec:\n description: the service descriptor for the http app\n groups: \n - http-app-group\n forceHttps: true\n hosts:\n - httpapp.foo.bar # hostname exposed oustide of the kubernetes cluster\n # - http-app-service-descriptor.global.otoroshi.mesh # implicit internal name inside the kubernetes cluster \n matchingRoot: /\n targets:\n - url: https://http-app-service:8443\n # alternatively, you can use serviceName and servicePort to use pods ip addresses\n # serviceName: http-app-service\n # servicePort: https\n mtlsConfig:\n # use mtls to contact the backend\n mtls: true\n certs: \n # reference the DN for the client cert\n - UID=httpapp-client, O=OtoroshiApps\n trustedCerts: \n # reference the DN for the CA cert \n - CN=Otoroshi Root\n sendOtoroshiHeadersBack: true\n xForwardedHeaders: true\n overrideHost: true\n allowHttp10: false\n publicPatterns:\n - /health\n additionalHeaders:\n x-foo: bar\n# here you can specify everything supported by otoroshi like jwt-verifiers, auth config, etc ... for more informations about it, just go to https://maif.github.io/otoroshi/swagger-ui/index.html\n```\n\nnow with this descriptor deployed, you can access your app with a command like \n\n```sh\nCLIENT_ID=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientId}\" | base64 --decode`\nCLIENT_SECRET=`kubectl get secret secret-1 -o jsonpath=\"{.data.clientSecret}\" | base64 --decode`\ncurl -X GET https://httpapp.foo.bar/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n## Expose Otoroshi to outside world\n\nIf you deploy Otoroshi on a kubernetes cluster, the Otoroshi service is deployed as a loadbalancer (service type: `LoadBalancer`). You'll need to declare in your DNS settings any name that can be routed by otoroshi going to the loadbalancer endpoint (CNAME or ip addresses) of your kubernetes distribution. If you use a managed kubernetes cluster from a cloud provider, it will work seamlessly as they will provide external loadbalancers out of the box. However, if you use a bare metal kubernetes cluster, id doesn't come with support for external loadbalancers (service of type `LoadBalancer`). So you will have to provide this feature in order to route external TCP traffic to Otoroshi containers running inside the kubernetes cluster. You can use projects like [MetalLB](https://metallb.universe.tf/) that provide software `LoadBalancer` services to bare metal clusters or you can use and customize examples in the installation section.\n\n@@@ warning\nWe don't recommand running Otoroshi behind an existing ingress controller (or something like that) as you will not be able to use features like TCP proxying, TLS, mTLS, etc. Also, this additional layer of reverse proxy will increase call latencies.\n@@@ \n\n## Access a service from inside the k8s cluster\n\n### Using host header overriding\n\nYou can access any service referenced in otoroshi, through otoroshi from inside the kubernetes cluster by using the otoroshi service name (if you use a template based on https://github.com/MAIF/otoroshi/tree/master/kubernetes/base deployed in the otoroshi namespace) and the host header with the service domain like :\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET -H 'Host: httpapp.foo.bar' https://otoroshi-service.otoroshi.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using dedicated services\n\nit's also possible to define services that targets otoroshi deployment (or otoroshi workers deployment) and use then as valid hosts in otoroshi services \n\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: my-awesome-service\nspec:\n selector:\n # run: otoroshi-deployment\n # or in cluster mode\n run: otoroshi-worker-deployment\n ports:\n - port: 8080\n name: \"http\"\n targetPort: \"http\"\n - port: 8443\n name: \"https\"\n targetPort: \"https\"\n```\n\nand access it like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-namspace.svc.cluster.local:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using coredns integration\n\nYou can also enable the coredns integration to simplify the flow. You can use the the following keys in the plugin config :\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"coreDnsIntegration\": true, // enable coredns integration for intra cluster calls\n \"kubeSystemNamespace\": \"kube-system\", // the namespace where coredns is deployed\n \"corednsConfigMap\": \"coredns\", // the name of the coredns configmap\n \"otoroshiServiceName\": \"otoroshi-service\", // the name of the otoroshi service, could be otoroshi-workers-service\n \"otoroshiNamespace\": \"otoroshi\", // the namespace where otoroshi is deployed\n \"clusterDomain\": \"cluster.local\", // the domain for cluster services\n ...\n }\n}\n```\n\notoroshi will patch coredns config at startup then you can call your services like\n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh` or `${service-name}.${service-namespace}.svc.otoroshi.local`\n\n### Using coredns with manual patching\n\nyou can also patch the coredns config manually\n\n```sh\nkubectl edit configmaps coredns -n kube-system # or your own custom config map\n```\n\nand change the `Corefile` data to add the following snippet in at the end of the file\n\n```yaml\notoroshi.mesh:53 {\n errors\n health\n ready\n kubernetes cluster.local in-addr.arpa ip6.arpa {\n pods insecure\n upstream\n fallthrough in-addr.arpa ip6.arpa\n }\n rewrite name regex (.*)\\.otoroshi\\.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n forward . /etc/resolv.conf\n cache 30\n loop\n reload\n loadbalance\n}\n```\n\nyou can also define simpler rewrite if it suits you use case better\n\n```\nrewrite name my-service.otoroshi.mesh otoroshi-worker-service.otoroshi.svc.cluster.local\n```\n\ndo not hesitate to change `otoroshi-worker-service.otoroshi` according to your own setup. If otoroshi is not in cluster mode, change it to `otoroshi-service.otoroshi`. If otoroshi is not deployed in the `otoroshi` namespace, change it to `otoroshi-service.the-namespace`, etc.\n\nBy default, all services created from CRDs service descriptors are exposed as `${service-name}.${service-namespace}.otoroshi.mesh`\n\nthen you can call your service like \n\n```sh\nCLIENT_ID=\"xxx\"\nCLIENT_SECRET=\"xxx\"\n\ncurl -X GET https://my-awesome-service.my-awesome-service-namespace.otoroshi.mesh:8443/get -u \"$CLIENT_ID:$CLIENT_SECRET\"\n```\n\n### Using old kube-dns system\n\nif your stuck with an old version of kubernetes, it uses kube-dns that is not supported by otoroshi, so you will have to provide your own coredns deployment and declare it as a stubDomain in the old kube-dns system. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the kube-dns integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"kubeDnsOperatorIntegration\": true, // enable kube-dns integration for intra cluster calls\n \"kubeDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"kubeDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"kubeDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\n### Using Openshift DNS operator\n\nOpenshift DNS operator does not allow to customize DNS configuration a lot, so you will have to provide your own coredns deployment and declare it as a stub in the Openshift DNS operator. \n\nHere is an example of coredns deployment with otoroshi domain config\n\ncoredns.yaml\n: @@snip [coredns.yaml](../snippets/kubernetes/kustomize/base/coredns.yaml)\n\nthen you can enable the Openshift DNS operator integration in the otoroshi kubernetes job\n\n```javascript\n{\n \"KubernetesConfig\": {\n ...\n \"openshiftDnsOperatorIntegration\": true, // enable openshift dns operator integration for intra cluster calls\n \"openshiftDnsOperatorCoreDnsNamespace\": \"otoroshi\", // namespace where coredns is installed\n \"openshiftDnsOperatorCoreDnsName\": \"otoroshi-dns\", // name of the coredns service\n \"openshiftDnsOperatorCoreDnsPort\": 5353, // port of the coredns service\n ...\n }\n}\n```\n\ndon't forget to update the otoroshi `ClusterRole`\n\n```yaml\n- apiGroups:\n - operator.openshift.io\n resources:\n - dnses\n verbs:\n - get\n - list\n - watch\n - update\n```\n\n## CRD validation in kubectl\n\nIn order to get CRD validation before manifest deployments right inside kubectl, you can deploy a validation webhook that will do the trick. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator` request sink enabled.\n\nvalidation-webhook.yaml\n: @@snip [validation-webhook.yaml](../snippets/kubernetes/kustomize/base/validation-webhook.yaml)\n\n## Easier integration with otoroshi-sidecar\n\nOtoroshi can help you to easily use existing services without modifications while gettings all the perks of otoroshi like apikeys, mTLS, exchange protocol, etc. To do so, otoroshi will inject a sidecar container in the pod of your deployment that will handle call coming from otoroshi and going to otoroshi. To enable otoroshi-sidecar, you need to deploy the following admission webhook. Also check that you have `otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector` request sink enabled.\n\nsidecar-webhook.yaml\n: @@snip [sidecar-webhook.yaml](../snippets/kubernetes/kustomize/base/sidecar-webhook.yaml)\n\nthen it's quite easy to add the sidecar, just add the following label to your pod `otoroshi.io/sidecar: inject` and some annotations to tell otoroshi what certificates and apikeys to use.\n\n```yaml\nannotations:\n otoroshi.io/sidecar-apikey: backend-apikey\n otoroshi.io/sidecar-backend-cert: backend-cert\n otoroshi.io/sidecar-client-cert: oto-client-cert\n otoroshi.io/token-secret: secret\n otoroshi.io/expected-dn: UID=oto-client-cert, O=OtoroshiApps\n```\n\nnow you can just call you otoroshi handled apis from inside your pod like `curl http://my-service.namespace.otoroshi.mesh/api` without passing any apikey or client certificate and the sidecar will handle everything for you. Same thing for call from otoroshi to your pod, everything will be done in mTLS fashion with apikeys and otoroshi exchange protocol\n\nhere is a full example\n\nsidecar.yaml\n: @@snip [sidecar.yaml](../snippets/kubernetes/kustomize/base/sidecar.yaml)\n\n@@@ warning\nPlease avoid to use port `80` for your pod as it's the default port to access otoroshi from your pod and the call will be redirect to the sidecar via an iptables rule\n@@@\n\n## Daikoku integration\n\nIt is possible to easily integrate daikoku generated apikeys without any human interaction with the actual apikey secret. To do that, create a plan in Daikoku and setup the integration mode to `Automatic`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen when a user subscribe for an apikey, he will only see an integration token\n\n@@@ div { .centered-img }\n\n@@@\n\nthen just create an ApiKey manifest with this token and your good to go \n\n```yaml\napiVersion: proxy.otoroshi.io/v1\nkind: ApiKey\nmetadata:\n name: http-app-2-apikey-3\nspec:\n exportSecret: true \n secretName: secret-3\n daikokuToken: RShQrvINByiuieiaCBwIZfGFgdPu7tIJEN5gdV8N8YeH4RI9ErPYJzkuFyAkZ2xy\n```\n\n"},{"name":"scaling.md","id":"/deploy/scaling.md","url":"/deploy/scaling.html","title":"Scaling Otoroshi","content":"# Scaling Otoroshi\n\n## Using multiple instances with a front load balancer\n\nOtoroshi has been designed to work with multiple instances. If you already have an infrastructure using frontal load balancing, you just have to declare Otoroshi instances as the target of all domain names handled by Otoroshi\n\n## Using master / workers mode of Otoroshi\n\nYou can read everything about it in @ref:[the clustering section](../deploy/clustering.md) of the documentation.\n\n## Using IPVS\n\nYou can use [IPVS](https://en.wikipedia.org/wiki/IP_Virtual_Server) to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi. You can find example of configuration [here](http://www.linuxvirtualserver.org/VS-DRouting.html) \n\n## Using DNS Round Robin\n\nYou can use [DNS round robin technique](https://en.wikipedia.org/wiki/Round-robin_DNS) to declare multiple A records under the domain names handled by Otoroshi.\n\n## Using software L4/L7 load balancers\n\nYou can use software L4 load balancers like NGINX or HAProxy to load balance layer 4 traffic directly from the Linux Kernel to multiple instances of Otoroshi.\n\nNGINX L7\n: @@snip [nginx-http.conf](../snippets/nginx-http.conf) \n\nNGINX L4\n: @@snip [nginx-tcp.conf](../snippets/nginx-tcp.conf) \n\nHA Proxy L7\n: @@snip [haproxy-http.conf](../snippets/haproxy-http.conf) \n\nHA Proxy L4\n: @@snip [haproxy-tcp.conf](../snippets/haproxy-tcp.conf) \n\n## Using a custom TCP load balancer\n\nYou can also use any other TCP load balancer, from a hardware box to a small js file like\n\ntcp-proxy.js\n: @@snip [tcp-proxy.js](../snippets/tcp-proxy.js) \n\ntcp-proxy.rs\n: @@snip [tcp-proxy.rs](../snippets/proxy.rs) \n\n"},{"name":"dev.md","id":"/dev.md","url":"/dev.html","title":"Developing Otoroshi","content":"# Developing Otoroshi\n\nIf you want to play with Otoroshis code, here are some tips\n\n## The tools\n\nYou will need\n\n* git\n* JDK >= 11\n* SBT >= 1.7+\n* Node 18+ & yarn 1.x\n\n## Clone the repository\n\n```sh\ngit clone https://github.com/MAIF/otoroshi.git\n```\n\nor fork otoroshi and clone your own repository.\n\n## Run otoroshi in dev mode\n\nto run otoroshi in dev mode, you'll need to run two separate process to serve the javascript UI and the server part.\n\n### Javascript side\n\njust go to `/otoroshi/javascript` and install the dependencies with\n\n```sh\nyarn install\n# or\nnpm install\n```\n\nthen run the dev server with\n\n```sh\nyarn start\n# or\nnpm run start\n```\n\n### Server side\n\nsetup SBT opts with\n\n```sh\nexport SBT_OPTS=\"-Xmx2G -Xss6M\"\n```\n\nthen just go to `/otoroshi` and run the sbt console with \n\n```sh\nsbt\n```\n\nthen in the sbt console run the following command\n\n```sh\n~reStart\n# to pass jvm args, you can use: ~reStart --- -Dotoroshi.storage=memory ...\n```\n\nyou can now access your otoroshi instance at `http://otoroshi.oto.tools:9999`\n\n## Test otoroshi\n\nto run otoroshi test just go to `/otoroshi` and run the main test suite with\n\n```sh\nsbt 'testOnly OtoroshiTests'\n```\n\n## Create a release\n\njust go to `/otoroshi/javascript` and then build the UI\n\n```sh\nyarn install\nyarn build\n```\n\nthen go to `/otoroshi` and build the otoroshi distribution\n\n```sh\nsbt ';clean;compile;dist;assembly'\n```\n\nthe otoroshi build is waiting for you in `/otoroshi/target/scala-2.12/otoroshi.jar` or `/otoroshi/target/universal/otoroshi-1.x.x.zip`\n\n## Build the documentation\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/doc.sh all\n```\n\nThe documentation is located at `manual/target/paradox/site/main/`\n\n## Format the sources\n\nfrom the root of your repository run\n\n```sh\nsh ./scripts/fmt.sh\n```\n"},{"name":"apikeys.md","id":"/entities/apikeys.md","url":"/entities/apikeys.html","title":"Apikeys","content":"# Apikeys\n\nAn API key is a unique identifier used to connect to, or perform, an route call. \n\n@@@ div { .centered-img }\n\n@@@\n\nYou can found a concrete example @ref:[here](../how-to-s/secure-with-apikey.md)\n\n* `ApiKey Id`: the id is a unique random key that will represent this API key\n* `ApiKey Secret`: the secret is a random key used to validate the API key\n* `ApiKey Name`: a name for the API key, used for debug purposes\n* `ApiKey description`: a useful description for this apikey\n* `Valid until`: auto disable apikey after this date\n* `Enabled`: if the API key is disabled, then any call using this API key will fail\n* `Read only`: if the API key is in read only mode, every request done with this api key will only work for GET, HEAD, OPTIONS verbs\n* `Allow pass by clientid only`: here you allow client to only pass client id in a specific header in order to grant access to the underlying api\n* `Constrained services only`: this apikey can only be used on services using apikey routing constraints\n* `Authorized on`: the groups/services linked to this api key\n\n### Metadata and tags\n\n* `Tags`: tags attached to the api key\n* `Metadata`: metadata attached to the api key\n\n### Automatic secret rotation\n\nAPI can handle automatic secret rotation by themselves. When enabled, the rotation changes the secret every `Rotation every` duration. During the `Grace period` both secret will be usable.\n \n* `Enabled`: enabled automatic apikey secret rotation\n* `Rotation every`: rotate secrets every\n* `Grace period`: period when both secrets can be used\n* `Next client secret`: display the next generated client secret\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Call examples\n\n* `Curl Command`: simple request with the api key passed by header\n* `Basic Auth. Header`: authorization Header with the api key as base64 encoded format\n* `Curl Command with Basic Auth. Header`: simple request with api key passed in the Authorization header as base64 format\n\n### Quotas\n\n* `Throttling quota`: the authorized number of calls per second\n* `Daily quota`: the authorized number of calls per day\n* `Monthly quota`: the authorized number of calls per month\n\n@@@ warning\n\nDaily and monthly quotas are based on the following rules :\n\n* daily quota is computed between 00h00:00.000 and 23h59:59.999 of the current day\n* monthly qutoas is computed between the first day of the month at 00h00:00.000 and the last day of the month at 23h59:59.999\n@@@\n\n### Quotas consumption\n\n* `Consumed daily calls`: the number of calls consumed today\n* `Remaining daily calls`: the remaining number of calls for today\n* `Consumed monthly calls`: the number of calls consumed this month\n* `Remaining monthly calls`: the remaining number of calls for this month\n\n"},{"name":"auth-modules.md","id":"/entities/auth-modules.md","url":"/entities/auth-modules.html","title":"Authentication modules","content":"# Authentication modules\n\nThe authentication modules manage the access to Otoroshi UI and can protect a route.\n\nA `private Otoroshi app` is an Otoroshi route with the Authentication plugin enabled.\n\nThe list of supported authentication are :\n\n* `OAuth 2.0/2.1` : an authorization standard that allows a user to grant limited access to their resources on one site to another site, without having to expose their credentials\n* `OAuth 1.0a` : the original standard for access delegation\n* `In memory` : create users directly in Otoroshi with rights and metadata\n* `LDAP : Lightweight Directory Access Protocol` : connect users using a set of LDAP servers\n* `SAML V2 - Security Assertion Markup Language` : an open-standard, XML-based data format that allows businesses to communicate user authentication and authorization information to partner companies and enterprise applications their employees may use.\n\nAll authentication modules have a unique `id`, a `name` and a `description`.\n\nEach module has also the following fields : \n\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n* `HttpOnly`: if enabled, the cookie cannot be accessed through client side script, prevent cross-site scripting (XSS) by not revealing the cookie to a third party\n* `Secure`: if enabled, avoid to include cookie in an HTTP Request without secure channel, typically HTTPs.\n* `Session max. age`: duration until the session expired\n* `User validators`: a list of validator that will check if, a user that successfully logged in has the right to actually, pass otoroshi based on the content of it's profile. A validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value. The JSONPath will be applied on a document that will look like\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"randomId\": \"xxxxx\",\n \"name\": \"john.doe@otoroshi.io\",\n \"email\": \"john.doe@otoroshi.io\",\n \"authConfigId\": \"xxxxxxxx\",\n \"profile\": { // the profile shape depends heavily on the identity provider\n \"sub\": \"xxxxxx\",\n \"nickname\": \"john.doe\",\n \"name\": \"john.doe@otoroshi.io\",\n \"picture\": \"https://foo.bar/avatar.png\",\n \"updated_at\": \"2022-04-20T12:57:39.723Z\",\n \"email\": \"john.doe@otoroshi.io\",\n \"email_verified\": true,\n \"rights\": [\"one\", \"two\"]\n },\n \"token\": { // the token shape depends heavily on the identity provider\n \"access_token\": \"xxxxxx\",\n \"refresh_token\": \"yyyyyy\",\n \"id_token\": \"zzzzzz\",\n \"scope\": \"openid profile email address phone offline_access\",\n \"expires_in\": 86400,\n \"token_type\": \"Bearer\"\n },\n \"realm\": \"global-oauth-xxxxxxx\",\n \"otoroshiData\": {\n ...\n },\n \"createdAt\": 1650459462650,\n \"expiredAt\": 1650545862652,\n \"lastRefresh\": 1650459462650,\n \"metadata\": {},\n \"tags\": []\n}\n```\n\nthe expected value support some syntax tricks like \n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current user has the right `two`, you can write the following validator\n\n```js\n{\n \"path\": \"$.profile.rights\",\n \"value\": \"Contains(two)\"\n}\n```\n\n## OAuth 2.0 / OIDC provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check these tutorials : @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md) or @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n\n* `Use cookie`: If your OAuth2 provider does not support query param in redirect uri, you can use cookies instead\n* `Use json payloads`: the access token, sended to retrieve the user info, will be pass in body as JSON. If disabled, it will sended as Map.\n* `Enabled PKCE flow`: This way, a malicious attacker can only intercept the Authorization Code, and they cannot exchange it for a token without the Code Verifier.\n* `Disable wildcard on redirect URIs`: As of OAuth 2.1, query parameters on redirect URIs are no longer allowed\n* `Refresh tokens`: Automatically refresh access token using the refresh token if available\n* `Read profile from token`: if enabled, the user profile will be read from the access token, otherwise the user profile will be retrieved from the user information url\n* `Super admins only`: All logged in users will have super admins rights\n* `Client ID`: a public identifier of your app\n* `Client Secret`: a secret known only to the application and the authorization server\n* `Authorize URL`: used to interact with the resource owner and get the authorization to access the protected resource\n* `Token URL`: used by the application in order to get an access token or a refresh token\n* `Introspection URL`: used to validate access tokens\n* `Userinfo URL`: used to retrieve the profile of the user\n* `Login URL`: used to redirect user to the login provider page\n* `Logout URL`: redirect uri used by the identity provider to redirect user after logging out\n* `Callback URL`: redirect uri sended to the identity provider to redirect user after successfully connecting\n* `Access token field name`: field used to search access token in the response body of the token URL call\n* `Scope`: presented scopes to the user in the consent screen. Scopes are space-separated lists of identifiers used to specify what access privileges are being requested\n* `Claims`: asked name/values pairs that contains information about a user.\n* `Name field name`: Retrieve name from token field\n* `Email field name`: Retrieve email from token field\n* `Otoroshi metadata field name`: Retrieve metadata from token field\n* `Otoroshi rights field name`: Retrieve user rights from user profile\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n* `Api key metadata field name`: used to extract api key metadata from the OIDC access token \n* `Api key tags field name`: used to extract api key tags from the OIDC access token \n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n* `OIDC config url`: URI of the openid-configuration used to discovery documents. By convention, this URI ends with `.well-known/openid-configuration`\n* `Token verification`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: The Hmac secret\n* `Base64 encoded secret`: Is the secret encoded with base64\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n## OAuth 1.0a provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)\n\n* `Http Method`: method used to get request token and the access token \n* `Consumer key`: the identifier portion of the client credentials (equivalent to a username)\n* `Consumer secret`: the identifier portion of the client credentials (equivalent to a password)\n* `Request Token URL`: url to retrieve the request token\n* `Authorize URL`: used to redirect user to the login page\n* `Access token URL`: used to retrieve the access token from the server\n* `Profile URL`: used to get the user profile\n* `Callback URL`: used to redirect user when successfully connecting\n* `Rights override`: override the rights of the connected user. With JSON format, each authenticated user, using email, can be associated to a list of rights on tenants and Otoroshi teams.\n\n## LDAP Authentication provider\n\nIf you want to secure an app or your Otoroshi UI with this provider, you can check this tutorial : @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Allow empty password`: LDAP servers configured by default with the possibility to connect without password can be secured by this module to ensure that user provides a password\n* `Super admins only`: All logged in users will have super admins rights\n* `Extract profile`: extract LDAP profile in the Otoroshi user\n* `LDAP Server URL`: list of LDAP servers to join. Otoroshi use this list in sequence and swap to the next server, each time a server breaks in timeout\n* `Search Base`: used to global filter\n* `Users search base`: concat with search base to search users in LDAP\n* `Mapping group filter`: map LDAP groups with Otoroshi rights\n* `Search Filter`: used to filter users. *\\${username}* is replace by the email of the user and compare to the given field\n* `Admin username (bind DN)`: holds the name of the environment property for specifying the identity of the principal for authenticating the caller to the service\n* `Admin password`: holds the name of the environment property for specifying the credentials of the principal for authenticating the caller to the service\n* `Extract profile filters attributes in`: keep only attributes which are matching the regex\n* `Extract profile filters attributes not in`: keep only attributes which are not matching the regex\n* `Name field name`: Retrieve name from LDAP field\n* `Email field name`: Retrieve email from LDAP field\n* `Otoroshi metadata field name`: Retrieve metadata from LDAP field\n* `Extra metadata`: merged with the user metadata\n* `Data override`: merged with extra metadata when a user connects to a `private app`\n* `Additional rights group`: list of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n* `Rights override`: useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights.\n\n## In memory provider\n\n* `Basic auth.`: if enabled, user and password will be extract from the `Authorization` header as a Basic authentication. It will skipped the login Otoroshi page \n* `Login with WebAuthn` : enabled logging by WebAuthn\n* `Users`: list of users with *name*, *email* and *metadata*. The default password is *password*. The edit button is useful when you want to change the password of the user. The reset button reinitialize the password. \n* `Users raw`: show the registered users with their profile and their rights. You can edit directly each field, especially the rights of the user.\n\n## SAML v2 provider\n\n* `Single sign on URL`: the Identity Provider Single Sign-On URL\n* `The protocol binding for the login request`: the protocol binding for the login request\n* `Single Logout URL`: a SAML flow that allows the end-user to logout from a single session and be automatically logged out of all related sessions that were established during SSO\n* `The protocol binding for the logout request`: the protocol binding for the logout request\n* `Sign documents`: Should SAML Request be signed by Otoroshi ?\n* `Validate Assertions Signature`: Enable/disable signature validation of SAML assertions\n* `Validate assertions with Otoroshi certificate`: validate assertions with Otoroshi certificate. If disabled, the `Encryption Certificate` and `Encryption Private Key` fields can be used to pass a certificate and a private key to validate assertions.\n* `Encryption Certificate`: certificate used to verify assertions\n* `Encryption Private Key`: privaye key used to verify assertions\n* `Signing Certificate`: certicate used to sign documents\n* `Signing Private Key`: private key to sign documents\n* `Signature al`: the signature algorithm to use to sign documents\n* `Canonicalization Method`: canonicalization method for XML signatures \n* `Encryption KeyPair`: the keypair used to sign/verify assertions\n* `Name ID Format`: SP and IdP usually communicate each other about a subject. That subject should be identified through a NAME-IDentifier, which should be in some format so that It is easy for the other party to identify it based on the Format\n* `Use NameID format as email`: use NameID format as email. If disabled, the email will be search from the attributes\n* `URL issuer`: provide the URL to the IdP's who will issue the security token\n* `Validate Signature`: enable/disable signature validation of SAML responses\n* `Validate Assertions Signature`: should SAML Assertions to be decrypted ?\n* `Validating Certificates`: the certificate in PEM format that must be used to check for signatures.\n\n## Special routes\n\nwhen using private apps with auth. modules, you can access special routes that can help you \n\n```sh \nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/logout' # trigger logout for the current auth. module\nGET 'http://xxxxxxxx.xxxx.xx/.well-known/otoroshi/me' # get the current logged user profile (do not forget to pass cookies)\n```\n\n## Related pages\n* @ref[Secure an app with auth0](../how-to-s/secure-app-with-auth0.md)\n* @ref[Secure an app with keycloak](../how-to-s/secure-app-with-keycloak.md)\n* @ref[Secure an app with LDAP](../how-to-s/secure-app-with-ldap.md)\n* @ref[Secure an app with OAuth 1.0a](../how-to-s/secure-with-oauth1-client.md)"},{"name":"backends.md","id":"/entities/backends.md","url":"/entities/backends.html","title":"Backends","content":"# Backends\n\nA backend represent a list of server to target in a route and its client settings, load balancing, etc.\n\nThe backends can be define directly on the route designer or on their dedicated page in order to be reusable.\n\n## UI page\n\nYou can find all backends [here](http://otoroshi.oto.tools:8080/bo/dashboard/backends)\n\n## Global Properties\n\n* `Targets root path`: the path to add to each request sent to the downstream service \n* `Full path rewrite`: When enabled, the path of the uri will be totally stripped and replaced by the value of `Targets root path`. If this value contains expression language expressions, they will be interpolated before forwading the request to the backend. When combined with things like named path parameters, it is possible to perform a ful url rewrite on the target path like\n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Targets\n\nThe list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures.\n\n* `id`: unique id of the target\n* `Hostname`: the hostname of the target without scheme\n* `Port`: the port of the target\n* `TLS`: call the target via https\n* `Weight`: the weight of the target. This valus is used by the load balancing strategy to dispatch the traffic between all targets\n* `Predicate`: a function to filter targets from the target list based on a predefined predicate\n* `Protocol`: protocol used to call the target, can be only equals to `HTTP/1.0`, `HTTP/1.1`, `HTTP/2.0` or `HTTP/3.0`\n* `IP address`: the ip address of the target\n* `TLS Settings`:\n * `Enabled`: enable this section\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with the downstream service\n * `Trusted certificates`: list of trusted certificates received from the downstream service\n\n\n## Heatlh check\n\n* `Enabled`: if enabled, the health check URL will be called at regular intervals\n* `URL`: the URL to call to run the health check\n\n## Load balancing\n\n* `Type`: the load balancing algorithm used\n\n## Client settings\n\n* `backoff factor`: specify the factor to multiply the delay for each retry (default value 2)\n* `retries`: specify how many times the client will retry to fetch the result of the request after an error before giving up. (default value 1)\n* `max errors`: specify how many errors can pass before opening the circuit breaker (default value 20)\n* `global timeout`: specify how long the global call (with retries) should last at most in milliseconds. (default value 30000)\n* `connection timeout`: specify how long each connection should last at most in milliseconds. (default value 10000)\n* `idle timeout`: specify how long each connection can stay in idle state at most in milliseconds (default value 60000)\n* `call timeout`: Specify how long each call should last at most in milliseconds. (default value 30000)\n* `call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response. (default value 120000)\n* `initial delay`: delay after which first retry will happens if needed (default value 50)\n* `sample interval`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor (default value 2000)\n* `cache connection`: try to keep tcp connection alive between requests (default value false)\n* `cache connection queue size`: queue size for an open tcp connection (default value 2048)\n* `custom timeouts` (list): \n * `Path`: the path on which the timeout will be active\n * `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n * `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n * `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n * `Call timeout`: Specify how long each call should last at most in milliseconds.\n * `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n## Proxy\n\n* `host`: host of proxy behind the identify provider\n* `port`: port of proxy behind the identify provider\n* `protocol`: protocol of proxy behind the identify provider\n* `principal`: user of proxy \n* `password`: password of proxy\n"},{"name":"certificates.md","id":"/entities/certificates.md","url":"/entities/certificates.html","title":"Certificates","content":"# Certificates\n\nAll generated and imported certificates are listed in the `https://otoroshi.xxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe list of available actions are:\n\n* `Add item`: redirects the user on the certificate creation page. It's useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* `Let's Encrypt certificate`: asks a certificate matching a given host to Let's encrypt \n* `Create certificate`: issues a certificate with an existing Otoroshi certificate as CA.\n* `Import .p12 file`: loads a p12 file as certificate\n\n## Add item\n\n* `Id`: the generated unique id of the certificate\n* `Name`: the name of the certificate\n* `Description`: the description of the certificate\n* `Auto renew cert.`: certificate will be issued when it will be expired. Only works with a CA from Otoroshi and a known private key\n* `Client cert.`: the certificate generated will be used to identicate a client to a server\n* `Keypair`: the certificate entity will be a pair of public key and private key.\n* `Public key exposed`: if true, the public key will be exposed on `http://otoroshi-api.your-domain/.well-known/jwks.json`\n* `Certificate status`: the current status of the certificate. It can be valid if the certificate is not revoked and not expired, or equal to the reason of the revocation\n* `Certificate full chain`: list of certificates used to authenticate a client or a server\n* `Certificate private key`: the private key of the certificate or nothing if wanted. You can omit it if you want just add a certificte full chain to trust them.\n* `Private key password`: the password to protect the private key\n* `Certificate tags`: the tags attached to the certificate\n* `Certaificate metadata`: the metadata attached to the certificate\n\n## Let's Encrypt certificate\n\n* `Let's encrypt`: if enabled, the certificate will be generated by Let's Encrypt. If disabled, the user will be redirect to the `Create certificate` page\n* `Host`: the host send to Let's encrypt to issue the certificate\n\n## Create certificate view\n\n* `Issuer`: the CA used to sign your certificate\n* `CA certificate`: if enabled, the certificate will be used as an authority certificate. Once generated, it will be use as CA to sign the new certificates\n* `Let's Encrypt`: redirects to the Let's Encrypt page to request a certificate\n* `Client certificate`: the certificate generated will be used to identicate a client to a server\n* `Include A.I.A`: include authority information access urls in the certificate\n* `Key Type`: the type of the private key\n* `Key Size`: the size of the private key\n* `Signature Algorithm`: the signature algorithm used to sign the certificate\n* `Digest Algorithm`: the digest algorithm used\n* `Validity`: how much time your certificate will be valid\n* `Subject DN`: the subject DN of your certificate\n* `Hosts`: the hosts of your certificate\n\n"},{"name":"data-exporters.md","id":"/entities/data-exporters.md","url":"/entities/data-exporters.html","title":"Data exporters","content":"# Data exporters\n\nThe data exporters are the way to export alerts and events from Otoroshi to an external storage.\n\nTo try them, you can folllow @ref[this tutorial](../how-to-s/export-alerts-using-mailgun.md).\n\n## Common fields\n\n* `Type`: the type of event exporter\n* `Enabled`: enabled or not the exporter\n* `Name`: given name to the exporter\n* `Description`: the data exporter description\n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nAll exporters are split in three parts. The first and second parts are common and the last are specific by exporter.\n\n* `Filtering and projection` : section to filter the list of sent events and alerts. The projection field allows you to export only certain event fields and reduce the size of exported data. It's composed of `Filtering` and `Projection` fields. To get a full usage of this elements, read @ref:[this section](#matching-and-projections)\n* `Queue details`: set of fields to adjust the workers of the exporter. \n * `Buffer size`: if elements are pushed onto the queue faster than the source is consumed the overflow will be handled with a strategy specified by the user. Keep in memory the number of events.\n * `JSON conversion workers`: number of workers used to transform events to JSON format in paralell\n * `Send workers`: number of workers used to send transformed events\n * `Group size`: chunk up this stream into groups of elements received within a time window (the time window is the next field)\n * `Group duration`: waiting time before sending the group of events. If the group size is reached before the group duration, the events will be instantly sent\n \nFor the last part, the `Exporter configuration` will be detail individually.\n\n## Matching and projections\n\n**Filtering** is used to **include** or **exclude** some kind of events and alerts. For each include and exclude field, you can add a list of key-value. \n\nLet's say we only want to keep Otoroshi alerts\n```json\n{ \"include\": [{ \"@type\": \"AlertEvent\" }] }\n```\n\nOtoroshi provides a list of rules to keep only events with specific values. We will use the following event to illustrate.\n\n```json\n{\n \"foo\": \"bar\",\n \"type\": \"AlertEvent\",\n \"alert\": \"big-alert\",\n \"status\": 200,\n \"codes\": [\"a\", \"b\"],\n \"inner\": {\n \"foo\": \"bar\",\n \"bar\": \"foo\"\n }\n}\n```\n\nThe rules apply with the previous example as event.\n\n@@@div { #filtering }\n \n@@@\n\n\n\n**Projection** is a list of fields to export. In the case of an empty list, all the fields of an event will be exported. In other case, **only** the listed fields will be exported.\n\nLet's say we only want to keep Otoroshi alerts and only type, timestamp and id of each exported events\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nAn other possibility is to **rename** the exported field. This value will be the same but the exported field will have a different name.\n\nLet's say we want to rename all `@id` field with `unique-id` as key\n\n```json\n{ \"@id\": \"unique-id\" }\n```\n\nThe last possiblity is to retrieve a sub-object of an event. Let's say we want to get the name of each exported user of events.\n\n```json\n{ \"user\": { \"name\": true } }\n```\n\nYou can also expand the entire source object with \n\n```json\n{\n \"$spread\": true\n}\n```\n\nand the remove fields you don't want with \n\n```json\n{\n \"fieldthatidontwant\": false\n}\n```\n\nProjections allows object modification using jspath, for instance, this example will create a new `otoroshiHeaderKeys` field to exported events. This field will contains a string array containing every request header name.\n\n```json\n{\n \"otoroshiHeaderKeys\": {\n \"$path\": \"$.otoroshiHeadersIn.*.key\"\n }\n}\n```\n\nAlternativerly, projections also allow to use JQ to transform exported events\n\n```json\n{\n \"headerKeys\": {\n \"$jq\": \"[.headers[].key]\"\n }\n}\n```\n\nJQ filter also allows conditionnal filtering : transformation is applied only if given predicate is match. In the following example, `headerKeys` field will be valued only if `target.scheme` is `https`.\n\n```json\n{\n \"headerKeys\": {\n \"$jqIf\": {\n \"filter\": \"[.headers[].key]\",\n \"predicate\": {\n \"path\": \"target.scheme\",\n \"value\": \"https\"\n }\n }\n }\n}\n```\n\nSee [JQ manual](https://jqlang.github.io/jq/manual/) for complete syntax reference.\n\n## Elastic\n\nWith this kind of exporter, every matching event will be sent to an elastic cluster (in batch). It is quite useful and can be used in combination with [elastic read in global config](./global-config.html#analytics-elastic-dashboard-datasource-read-)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with checked point, and if the case of it's successfull, it will displayed the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template presents in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n## Webhook \n\nWith this kind of exporter, every matching event will be sent to a URL (in batch) using a POST method and an JSON array body.\n\n* `Alerts hook URL`: url used to post events\n* `Hook Headers`: headers add to the post request\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n## Pulsar \n\nWith this kind of exporter, every matching event will be sent to an [Apache Pulsar topic](https://pulsar.apache.org/)\n\n\n* `Pulsar URI`: URI of the pulsar server\n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n* `Pulsar tenant`: tenant on the pulsar server\n* `Pulsar namespace`: namespace on the pulsar server\n* `Pulsar topic`: topic on the pulsar server\n\n## Kafka \n\nWith this kind of exporter, every matching event will be sent to an [Apache Kafka topic](https://kafka.apache.org/). You can find few @ref[tutorials](../how-to-s/communicate-with-kafka.md) about the connection between Otoroshi and Kafka based on docker images.\n\n* `Kafka Servers`: the list of servers to contact to connect the Kafka client with the Kafka cluster\n* `Kafka topic`: the topic on which Otoroshi alerts will be sent\n\nBy default, Kafka is installed with no authentication. Otoroshi supports the following authentication mechanisms and protocols for Kafka brokers.\n\n### SASL\n\nThe Simple Authentication and Security Layer (SASL) [RFC4422] is a\nmethod for adding authentication support to connection-based\nprotocols.\n\n* `SASL username`: the client username \n* `SASL password`: the client username \n* `SASL Mechanism`: \n * `PLAIN`: SASL/PLAIN uses a simple username and password for authentication.\n * `SCRAM-SHA-256` and `SCRAM-SHA-512`: SASL/SCRAM uses usernames and passwords stored in ZooKeeper. Credentials are created during installation.\n\n### SSL \n\n* `Kafka keypass`: the keystore password if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka keystore path`: the keystore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Kafka truststore path`: the truststore path on the server if you use a keystore/truststore to connect to Kafka cluster\n* `Custom TLS Settings`: enable the TLS configuration for the communication with Elasticsearch\n * `TLS loose`: if enabled, will block all untrustful ssl configs\n * `TrustAll`: allows any server certificates even the self-signed ones\n * `Client certificates`: list of client certificates used to communicate with elasticsearch\n * `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n### SASL + SSL\n\nThis mechanism uses the SSL configuration and the SASL configuration.\n\n## Mailer \n\nWith this kind of exporter, every matching event will be sent in batch as an email (using one of the following email provider)\n\nOtoroshi supports 5 exporters of email type.\n\n### Console\n\nNothing to add. The events will be write on the standard output.\n\n### Generic\n\n* `Mailer url`: URL used to push events\n* `Headers`: headers add to the push requests\n* `Email addresses`: recipients of the emails\n\n### Mailgun\n\n* `EU`: is EU server ? if enabled, *https://api.eu.mailgun.net/* will be used, otherwise, the US URL will be used : *https://api.mailgun.net/*\n* `Mailgun api key`: API key of the mailgun account\n* `Mailgun domain`: domain name of the mailgun account\n* `Email addresses`: recipients of the emails\n\n### Mailjet\n\n* `Public api key`: public key of the mailjet account\n* `Private api key`: private key of the mailjet account\n* `Email addresses`: recipients of the emails\n\n### Sendgrid\n\n* `Sendgrid api key`: api key of the sendgrid account\n* `Email addresses`: recipients of the emails\n\n## File \n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n\n## GoReplay file\n\nWith this kind of exporter, every matching event will be sent to a `.gor` file compatible with [GoReplay](https://goreplay.org/). \n\n@@@ warning\nthis exporter will only be able to catch `TrafficCaptureEvent`. Those events are created when a route (or the global config) of the @ref:[new proxy engine](../topics/engine.md) is setup to capture traffic using the `capture` flag.\n@@@\n\n* `File path`: path where the logs will be write \n* `Max file size`: when size is reached, Otoroshi will create a new file postfixed by the current timestamp\n* `Capture requests`: capture http requests in the `.gor` file\n* `Capture responses`: capture http responses in the `.gor` file\n\n## Console \n\nNothing to add. The events will be write on the standard output.\n\n## Custom \n\nThis type of exporter let you the possibility to write your own exporter with your own rules. To create an exporter, we need to navigate to the plugins page, and to create a new item of type exporter.\n\nWhen it's done, the exporter will be visible in this list.\n\n* `Exporter config.`: the configuration of the custom exporter.\n\n## Metrics \n\nThis plugin is useful to rewrite the metric labels exposed on the `/metrics` endpoint.\n\n* `Labels`: list of metric labels. Each pair contains an existing field name and the new name."},{"name":"global-config.md","id":"/entities/global-config.md","url":"/entities/global-config.html","title":"Global config","content":"# Global config\n\nThe global config, named `Danger zone` in Otoroshi, is the place to configure Otoroshi globally. \n\n> Warning: In this page, the configuration is really sensitive and affects the global behaviour of Otoroshi.\n\n\n### Misc. Settings\n\n\n* `Maintenance mode` : It passes every single service in maintenance mode. If a user calls a service, the maintenance page will be displayed\n* `No OAuth login for BackOffice` : Forces admins to login only with user/password or user/password/u2F device\n* `API Read Only`: Freeze Otoroshi datastore in read only mode. Only people with access to the actual underlying datastore will be able to disable this.\n* `Auto link default` : When no group is specified on a service, it will be assigned to default one\n* `Use circuit breakers` : Use circuit breaker on all services\n* `Use new http client as the default Http client` : All http calls will use the new http client by default\n* `Enable live metrics` : Enable live metrics in the Otoroshi cluster. Performs a lot of writes in the datastore\n* `Digitus medius` : Use middle finger emoji as a response character for endless HTTP responses (see [IP address filtering settings](#ip-address-filtering-settings)).\n* `Limit conc. req.` : Limit the number of concurrent request processed by Otoroshi to a certain amount. Highly recommended for resilience\n* `Use X-Forwarded-* headers for routing` : When evaluating routing of a request, X-Forwarded-* headers will be used if presents\n* `Max conc. req.` : Maximum number of concurrent requests processed by otoroshi.\n* `Max HTTP/1.0 resp. size` : Maximum size of an HTTP/1.0 response in bytes. After this limit, response will be cut and sent as is. The best value here should satisfy (maxConcurrentRequests * maxHttp10ResponseSize) < process.memory for worst case scenario.\n* `Max local events` : Maximum number of events stored.\n* `Lines` : *deprecated* \n\n### IP address filtering settings\n\n* `IP allowed list`: Only IP addresses that will be able to access Otoroshi exposed services\n* `IP blocklist`: IP addresses that will be refused to access Otoroshi exposed services\n* `Endless HTTP Responses`: IP addresses for which each request will return around 128 Gb of 0s\n\n\n### Quotas settings\n\n* `Global throttling`: The max. number of requests allowed per second globally on Otoroshi\n* `Throttling per IP`: The max. number of requests allowed per second per IP address globally on Otoroshi\n\n### Analytics: Elastic dashboard datasource (read)\n\n* `Cluster URI`: Elastic cluster URI\n* `Index`: Elastic index \n* `Type`: Event type (not needed for elasticsearch above 6.x)\n* `User`: Elastic User (optional)\n* `Password`: Elastic password (optional)\n* `Version`: Elastic version (optional, if none provided it will be fetched from cluster)\n* `Apply template`: Automatically apply index template\n* `Check Connection`: Button to test the configuration. It will displayed a modal with a connection checklist, if connection is successfull, it will display the found version of the Elasticsearch and the index used\n* `Manually apply index template`: try to put the elasticsearch template by calling the api of elasticsearch\n* `Show index template`: try to retrieve the current index template present in elasticsearch\n* `Client side temporal indexes handling`: When enabled, Otoroshi will manage the creation of indexes over time. When it's disabled, Otoroshi will push in the same index\n* `One index per`: When the previous field is enabled, you can choose the interval of time between the creation of a new index in elasticsearch \n* `Custom TLS Settings`: Enable the TLS configuration for the communication with Elasticsearch\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `TrustAll`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with elasticsearch\n* `Trusted certificates`: list of trusted certificates received from elasticsearch\n\n\n### Statsd settings\n\n* `Datadog agent`: The StatsD agent is a Datadog agent\n* `StatsD agent host`: The host on which StatsD agent is listening\n* `StatsD agent port`: The port on which StatsD agent is listening (default is 8125)\n\n\n### Backoffice auth. settings\n\n* `Backoffice auth. config`: the authentication module used in front of Otoroshi. It will be used to connect to Otoroshi on the login page\n\n### Let's encrypt settings\n\n* `Enabled`: when enabled, Otoroshi will have the possiblity to sign certificate from let's encrypt notably in the SSL/TSL Certificates page \n* `Server URL`: ACME endpoint of let's encrypt \n* `Email addresses`: (optional) list of addresses used to order the certificates \n* `Contact URLs`: (optional) list of addresses used to order the certificates \n* `Public Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n* `Private Key`: used to ask a certificate to let's encrypt, generated by Otoroshi \n\n\n### CleverCloud settings\n\nOnce configured, you can register one clever cloud app of your organization directly as an Otoroshi service.\n\n* `CleverCloud consumer key`: consumer key of your clever cloud OAuth 1.0 app\n* `CleverCloud consumer secret`: consumer secret of your clever cloud OAuth 1.0 app\n* `OAuth Token`: oauth token of your clever cloud OAuth 1.0 app\n* `OAuth Secret`: oauth token secret of your clever cloud OAuth 1.0 app \n* `CleverCloud orga. Id`: id of your clever cloud organization\n\n### Global scripts\n\nGlobal scripts will be deprecated soon, please use global plugins instead (see the next section)!\n\n### Global plugins\n\n* `Enabled`: enable the use of global plugins\n* `Plugins on new Otoroshi engine`: list of plugins used by the new Otoroshi engine\n* `Plugins on old Otoroshi engine`: list of plugins used by the old Otoroshi engine\n* `Plugin configuration`: the overloaded configuration of plugins\n\n### Proxies\n\nIn this section, you can add a list of proxies for :\n\n* Proxy for alert emails (mailgun)\n* Proxy for alert webhooks\n* Proxy for Clever-Cloud API access\n* Proxy for services access\n* Proxy for auth. access (OAuth, OIDC)\n* Proxy for client validators\n* Proxy for JWKS access\n* Proxy for elastic access\n\nEach proxy has the following fields \n\n* `Proxy host`: host of proxy\n* `Proxy port`: port of proxy\n* `Proxy principal`: user of proxy\n* `Proxy password`: password of proxy\n* `Non proxy host`: IP address that can access the service\n\n### Quotas alerting settings\n\n* `Enable quotas exceeding alerts`: When apikey quotas is almost exceeded, an alert will be sent \n* `Daily quotas threshold`: The percentage of daily calls before sending alerts\n* `Monthly quotas threshold`: The percentage of monthly calls before sending alerts\n\n### User-Agent extraction settings\n\n* `User-Agent extraction`: Allow user-agent details extraction. Can have impact on consumed memory. \n\n### Geolocation extraction settings\n\nExtract a geolocation for each call to Otoroshi.\n\n### Tls Settings\n\n* `Use random cert.`: Use the first available cert when none matches the current domain\n* `Default domain`: When the SNI domain cannot be found, this one will be used to find the matching certificate \n* `Trust JDK CAs (server)`: Trust JDK CAs. The CAs from the JDK CA bundle will be proposed in the certificate request when performing TLS handshake \n* `Trust JDK CAs (trust)`: Trust JDK CAs. The CAs from the JDK CA bundle will be used as trusted CAs when calling HTTPS resources \n* `Trusted CAs (server)`: Select the trusted CAs you want for TLS terminaison. Those CAs only will be proposed in the certificate request when performing TLS handshake \n\n\n### Auto Generate Certificates\n\n* `Enabled`: Generate certificates on the fly when they don't exist\n* `Reply Nicely`: When receiving request from a not allowed domain name, accept connection and display a nice error message \n* `CA`: certificate CA used to generate missing certificate\n* `Allowed domains`: Allowed domains\n* `Not allowed domains`: Not allowed domains\n \n\n### Global metadata\n\n* `Tags`: tags attached to the global config\n* `Metadata`: metadata attached to the global config\n\n### Actions at the bottom of the page\n\n* `Recover from a full export file`: Load global configuration from a previous export\n* `Full export`: Export with all created entities\n* `Full export (ndjson)`: Export your full state of database to ndjson format\n* `JSON`: Get the global config at JSON format \n* `YAML`: Get the global config at YAML format \n* `Enable Panic Mode`: Log out all users from UI and prevent any changes to the database by setting the admin Otoroshi api to read-only. The only way to exit of this mode is to disable this mode directly in the database. "},{"name":"index.md","id":"/entities/index.md","url":"/entities/index.html","title":"","content":"\n# Main entities\n\nIn this section, we will pass through all the main Otoroshi entities. Otoroshi entities are the main items stored in otoroshi datastore that will be used to configure routing, authentication, etc.\n\nAny entity has the following properties\n\n* **location** or **\\_loc**: the location of the entity (organization and team)\n* **id**: the id of the entity (except for apikeys)\n* **name**: the name of the entity\n* **description**: the description of the entity (optional)\n* **tags**: free tags that you can put on any entity to help you manage it, automate it, etc.\n* **metadata**: free key/value tuples that you can put on any entity to help you manage it, automate it, etc.\n\n@@@div { .entities }\n\n
      \nRoutes\nProxy your applications with routes\n
      \n@ref:[View](./routes.md)\n@@@\n\n@@@div { .entities }\n\n
      \nBackends\nReuse route targets\n
      \n@ref:[View](./backends.md)\n@@@\n\n@@@div { .entities }\n\n
      \nApikeys\nAdd security to your services using apikeys\n
      \n@ref:[View](./apikeys.md)\n@@@\n\n\n@@@div { .entities }\n\n
      \nOrganizations\nThis the most high level for grouping resources.\n
      \n@ref:[View](./organizations.md)\n@@@\n\n@@@div { .entities }\n\n
      \nTeams\nOrganize your resources by teams\n
      \n@ref:[View](./teams.md)\n@@@\n\n@@@div { .entities }\n\n
      \nService groups\nGroup your services\n
      \n@ref:[View](./service-groups.md)\n@@@\n\n@@@div { .entities }\n\n
      \nJWT verifiers\nVerify and forge token by services.\n
      \n@ref:[View](./jwt-verifiers.md)\n@@@\n\n@@@div { .entities }\n\n
      \nGlobal Config\nThe danger zone of Otoroshi\n
      \n@ref:[View](./global-config.md)\n@@@\n\n@@@div { .entities }\n\n
      \nTCP services\n\n
      \n@ref:[View](./tcp-services.md)\n@@@\n\n@@@div { .entities }\n\n
      \nAuth. modules\nSecure the Otoroshi UI and your web apps\n
      \n@ref:[View](./auth-modules.md)\n@@@\n\n@@@div { .entities }\n\n
      \nCertificates\nAdd secure communication between Otoroshi, clients and services\n
      \n@ref:[View](./certificates.md)\n@@@\n\n@@@div { .entities }\n\n
      \nData exporters\nExport alerts, events ands logs\n
      \n@ref:[View](./data-exporters.md)\n@@@\n\n@@@div { .entities }\n\n
      \nScripts\n\n
      \n@ref:[View](./scripts.md)\n@@@\n\n@@@div { .entities }\n\n
      \nService descriptors\nProxy your applications with service descriptors\n
      \n@ref:[View](./service-descriptors.md)\n@@@\n\n@@@ index\n\n* [Routes](./routes.md)\n* [Backends](./backends.md)\n* [Organizations](./organizations.md)\n* [Teams](./teams.md)\n* [Global Config](./global-config.md)\n* [Apikeys](./apikeys.md)\n* [Service groups](./service-groups.md)\n* [Auth. modules](./auth-modules.md)\n* [Certificates](./certificates.md)\n* [JWT verifiers](./jwt-verifiers.md)\n* [Data exporters](./data-exporters.md)\n* [Scripts](./scripts.md)\n* [TCP services](./tcp-services.md)\n* [Service descriptors](./service-descriptors.md)\n\n@@@\n"},{"name":"jwt-verifiers.md","id":"/entities/jwt-verifiers.md","url":"/entities/jwt-verifiers.html","title":"JWT verifiers","content":"# JWT verifiers\n\nSometimes, it can be pretty useful to verify Jwt tokens coming from other provider on some services. Otoroshi provides a tool to do that per service.\n\n* `Name`: name of the JWT verifier\n* `Description`: a simple description\n* `Strict`: if not strict, request without JWT token will be allowed to pass. This option is helpful when you want to force the presence of tokens in each request on a specific service \n* `Tags`: list of tags associated to the module\n* `Metadata`: list of metadata associated to the module\n\nEach JWT verifier is configurable in three steps : the `location` where find the token in incoming requests, the `validation` step to check the signature and the presence of claims in tokens, and the last step, named `Strategy`.\n\n## Token location\n\nAn incoming token can be found in three places.\n\n#### In query string\n\n* `Source`: JWT token location in query string\n* `Query param name`: the name of the query param where JWT is located\n\n#### In a header\n\n* `Source`: JWT token location in a header\n* `Header name`: the name of the header where JWT is located\n* `Remove value`: when the token is read, this value will be remove of header value (example: if the header value is *Bearer xxxx*, the *remove value* could be Bearer  don't forget the space at the end of the string)\n\n#### In a cookie\n\n* `Source`: JWT token location in a cookie\n* `Cookie name`: the name of the cookie where JWT is located\n\n## Token validation\n\nThis section is used to verify the extracted token from specified location.\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n\nAccording to the selected algorithm, the validation form will change.\n\n#### Hmac + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n#### RSASSA-PKCS1 + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the RSA public key\n* `Private key`: the RSA private key that can be empty if not used for JWT token signing\n\n#### ECDSA + SHA\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Public key`: the ECDSA public key\n* `Private key`: the ECDSA private key that can be empty if not used for JWT token signing\n\n#### RSASSA-PKCS1 + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n \n#### ECDSA + SHA from KeyPair\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `KeyPair`: used to sign/verify token. The displayed list represents the key pair registered in the Certificates page\n\n#### Otoroshi KeyPair from token kid (only for verification)\n* `Use only exposed keypairs`: if enabled, Otoroshi will only use the key pairs that are exposed on the well-known. If disabled, it will search on any registered key pairs.\n\n#### JWK Set (only for verification)\n\n* `URL`: the JWK set URL where the public keys are exposed\n* `HTTP call timeout`: timeout for fetching the keyset\n* `TTL`: cache TTL for the keyset\n* `HTTP Headers`: the HTTP headers passed\n* `Key type`: type of the key searched in the jwks\n\n*TLS settings for JWKS fetching*\n\n* `Custom TLS Settings`: TLS settings for JWKS fetching\n* `TLS loose`: if enabled, will block all untrustful ssl configs\n* `Trust all`: allows any server certificates even the self-signed ones\n* `Client certificates`: list of client certificates used to communicate with JWKS server\n* `Trusted certificates`: list of trusted certificates received from JWKS server\n\n*Proxy*\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n## Strategy\n\nThe first step is to select the verifier strategy. Otoroshi supports 4 types of JWT verifiers:\n\n* `Default JWT token` will add a token if no present. \n* `Verify JWT token` will only verifiy token signing and fields values if provided. \n* `Verify and re-sign JWT token` will verify the token and will re-sign the JWT token with the provided algo. settings. \n* `Verify, re-sign and transform JWT token` will verify the token, re-sign and will be able to transform the token.\n\nAll verifiers has the following properties: \n\n* `Verify token fields`: when the JWT token is checked, each field specified here will be verified with the provided value\n* `Verify token array value`: when the JWT token is checked, each field specified here will be verified if the provided value is contained in the array\n\n\n#### Default JWT token\n\n* `Strict`: if token is already present, the call will fail\n* `Default value`: list of claims of the generated token. These fields support raw values or language expressions. See the documentation about @ref:[the expression language](../topics/expression-language.md)\n\n#### Verify JWT token\n\nNo specific values needed. This kind of verifier needs only the two fields `Verify token fields` and `Verify token array value`.\n\n#### Verify and re-sign JWT token\n\nWhen `Verify and re-sign JWT token` is chosen, the `Re-sign settings` appear. All fields of `Re-sign settings` are the same of the `Token validation` section. The only difference is that the values are used to sign the new token and not to validate the token.\n\n\n#### Verify, re-sign and transform JWT token\n\nWhen `Verify, re-sign and transform JWT token` is chosen, the `Re-sign settings` and `Transformation settings` appear.\n\nThe `Re-sign settings` are used to sign the new token and has the same fields than the `Token validation` section.\n\nFor the `Transformation settings` section, the fields are:\n\n* `Token location`: the location where to find/set the JWT token\n* `Header name`: the name of the header where JWT is located\n* `Prepend value`: remove a value inside the header value\n* `Rename token fields`: when the JWT token is transformed, it is possible to change a field name, just specify origin field name and target field name\n* `Set token fields`: when the JWT token is transformed, it is possible to add new field with static values, just specify field name and value\n* `Remove token fields`: when the JWT token is transformed, it is possible to remove fields"},{"name":"organizations.md","id":"/entities/organizations.md","url":"/entities/organizations.html","title":"Organizations","content":"# Organizations\n\nThe resources of Otoroshi are grouped by `Organization`. This the highest level for grouping resources.\n\nAn organization have a unique `id`, a `name` and a `description`. As all Otoroshi resources, an Organization have a list of tags and metadata associated.\n\nFor example, you can use the organizations as a mean of :\n\n* to seperate resources by services or entities in your enterprise\n* to split internal and external usage of the resources (it's useful when you have a list of services deployed in your company and another one deployed by your partners)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of organizations\n\nTo visualize and edit the list of organizations, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/organizations` route or click on the cog icon and select the organizations button.\n\nOnce on the page, you can create a new item, edit an existing organization or delete an existing one.\n\n> When an organization is deleted, the resources associated are not deleted. On the other hand, the organization and the team of associated resources are let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of one organization (`tenant` in the json document)\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": ...\n }\n ...\n}\n```\n\nor all organizations\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"*\",\n \"teams\": ...\n }\n ...\n}\n```\n\n"},{"name":"routes.md","id":"/entities/routes.md","url":"/entities/routes.html","title":"Routes","content":"# Routes\n\nA route is an unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins and eventually forward the request to the backend application.\n\n## UI page\n\nYou can find all routes [here](http://otoroshi.oto.tools:8080/bo/dashboard/routes)\n\n## Global Properties\n\n* `location`: the location of the entity\n* `id`: the id of the route\n* `name`: the name of the route\n* `description`: the description of the route\n* `tags`: the tags of the route. can be useful for api automation\n* `metadata`: the metadata of the route. can be useful for api automation. There are a few reserved metadata used by otorshi that can be found @ref[below](./routes.md#reserved-metadata)\n* `enabled`: is the route enabled ? if not, the router will not consider this route\n* `debugFlow`: the debug flag. If enabled, the execution report for this route will contain all input/output values through steps of the proxy engine. For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `capture`: if enabled, otoroshi will generate events containing the whole content of each request. Use with caution ! For more informations, check the @ref[engine documentation](../topics/engine.md#http-traffic-capture)\n* `exportReporting`: if enabled, execution reports of the proxy engine will be generated for each request. Those reports are exportable using @ref[data exporters](./data-exporters.md) . For more informations, check the @ref[engine documentation](../topics/engine.md#reporting)\n* `groups`: each route is attached to a group. A group can have one or more services/routes. Each API key is linked to groups/routes/services and allow access to every entities in the groups.\n\n### Reserved metadata\n\nsome metadata are reserved for otoroshi usage. Here is the list of reserved metadata\n\n* `otoroshi-core-user-facing`: is this a user facing app for the snow monkey\n* `otoroshi-core-use-akka-http-client`: use the pure akka http client\n* `otoroshi-core-use-netty-http-client`: use the pure netty http client\n* `otoroshi-core-use-akka-http-ws-client`: use the modern websocket client\n* `otoroshi-core-issue-lets-encrypt-certificate`: enabled let's encrypt certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate`: enabled certificate issue for this route. true or false\n* `otoroshi-core-issue-certificate-ca`: the id of the CA cert to generate the certificate for this route\n* `otoroshi-core-openapi-url`: the openapi url for this route\n* `otoroshi-core-env`: the env for this route. here for legacy reasons\n* `otoroshi-deployment-providers`: in the case of relay routing, the providers for this route\n* `otoroshi-deployment-regions`: in the case of relay routing, the network regions for this route\n* `otoroshi-deployment-zones`: in the case of relay routing, the network zone for this route \n* `otoroshi-deployment-dcs`: in the case of relay routing, the datacenter for this route \n* `otoroshi-deployment-racks`: in the case of relay routing, the rack for this route \n\n## Frontend configuration\n\n* `frontend`: the frontend of the route. It's the configuration that will configure how otoroshi router will match this route. A frontend has the following shape. \n\n```javascript\n{\n \"domains\": [ // the matched domains and paths\n \"new-route.oto.tools/path\" // here you can use wildcard in domain and path, also you can use named path params\n ],\n \"strip_path\": true, // is the matched path stripped in the forwarded request\n \"exact\": false, // perform exact matching on path, if not, will be matched on /path*\n \"headers\": {}, // the matched http headers. if none provided, any header will be matched\n \"query\": {}, // the matched http query params. if none provided, any query params will be matched\n \"methods\": [] // the matched http methods. if none provided, any method will be matched\n}\n```\n\nFor more informations about routing, check the @ref[engine documentation](../topics/engine.md#routing)\n\n## Backend configuration\n\n* `backend`: a backend to forward requests to. For more informations, go to the @ref[backend documentation](./backends.md)\n* `backendRef`: a reference to an existing backend id\n\n## Plugins\n\nthe liste of plugins used on this route. Each plugin definition has the following shape:\n\n```javascript\n{\n \"enabled\": false, // is the plugin enabled\n \"debug\": false, // is debug enabled of this specific plugin\n \"plugin\": \"cp:otoroshi.next.plugins.Redirection\", // the id of the plugin\n \"include\": [], // included paths. if none, all paths are included\n \"exclude\": [], // excluded paths. if none, none paths are excluded\n \"config\": { // the configuration of the plugin\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"plugin_index\": { // the position of the plugin. if none provided, otoroshi will use the order in the plugin array\n \"pre_route\": 0\n }\n}\n```\n\nfor more informations about the available plugins, go @ref[here](../plugins/built-in-plugins.md)\n\n\n"},{"name":"scripts.md","id":"/entities/scripts.md","url":"/entities/scripts.html","title":"Scripts","content":"# Scripts\n\nScript are a way to create plugins for otoroshi without deploying them as jar files. With scripts, you just have to store the scala code of your plugins inside the otoroshi datastore and otoroshi will compile and deploy them at startup. You can find all your scripts in the UI at `cog icon / Plugins`. You can find all the documentation about plugins @ref:[here](../plugins/index.md)\n\n@@@ warning\nThe compilation of your plugins can be pretty long and resources consuming. As the compilation happens during otoroshi boot sequence, your instance will be blocked until all plugins have compiled. This behavior can be disabled. If so, the plugins will not work until they have been compiled. Any service using a plugin that is not compiled yet will fail\n@@@\n\nLike any entity, the script has has the following properties\n\n* `id`\n* `plugin name`\n* `plugin description`\n* `tags`\n* `metadata`\n\nAnd you also have\n\n* `type`: the kind of plugin you are building with this script\n* `plugin code`: the code for your plugin\n\n## Compile\n\nYou can use the compile button to check if the code you write in `plugin code` is valid. It will automatically save your script and try to compile. As mentionned earlier, script compilation is quite resource intensive. It will affect your CPU load and your memory consumption. Don't forget to adjust your VM settings accordingly.\n"},{"name":"service-descriptors.md","id":"/entities/service-descriptors.md","url":"/entities/service-descriptors.html","title":"Service descriptors","content":"# Service descriptors\n\nServices or service descriptor, let you declare how to proxy a call from a domain name to another domain name (or multiple domain names). \n\n@@@ div { .centered-img }\n\n@@@\n\nLet’s say you have an API exposed on http://192.168.0.42 and I want to expose it on https://my.api.foo. Otoroshi will proxy all calls to https://my.api.foo and forward them to http://192.168.0.42. While doing that, it will also log everyhting, control accesses, etc.\n\n\n* `Id`: a unique random string to identify your service\n* `Groups`: each service descriptor is attached to a group. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group.\n* `Create a new group`: you can create a new group to host this descriptor\n* `Create dedicated group`: you can create a new group with an auto generated name to host this descriptor\n* `Name`: the name of your service. Only for debug and human readability purposes.\n* `Description`: the description of your service. Only for debug and human readability purposes.\n* `Service enabled`: activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist.\n* `Read only mode`: authorize only GET, HEAD, OPTIONS calls on this service\n* `Maintenance mode`: display a maintainance page when a user try to use the service\n* `Construction mode`: display a construction page when a user try to use the service\n* `Log analytics`: Log analytics events for this service on the servers\n* `Use new http client`: will use Akka Http Client for every request\n* `Detect apikey asap`: If the service is public and you provide an apikey, otoroshi will detect it and validate it. Of course this setting may impact performances because of useless apikey lookups.\n* `Send Otoroshi headers back`: when enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...\n* `Override Host header`: when enabled, Otoroshi will automatically set the Host header to corresponding target host\n* `Send X-Forwarded-* headers`: when enabled, Otoroshi will send X-Forwarded-* headers to target\n* `Force HTTPS`: will force redirection to `https://` if not present\n* `Allow HTTP/1.0 requests`: will return an error on HTTP/1.0 request\n* `Use new WebSocket client`: will use the new websocket client for every websocket request\n* `TCP/UDP tunneling`: with this setting enabled, otoroshi will not proxy http requests anymore but instead will create a secured tunnel between a cli on your machine and otoroshi to proxy any tcp connection with all otoroshi security features enabled\n\n### Service exposition settings\n\n* `Exposed domain`: the domain used to expose your service. Should follow pattern: `(http|https)://subdomain?.env?.domain.tld?/root?` or regex `(http|https):\\/\\/(.*?)\\.?(.*?)\\.?(.*?)\\.?(.*)\\/?(.*)`\n* `Legacy domain`: use `domain`, `subdomain`, `env` and `matchingRoot` for routing in addition to hosts, or just use hosts.\n* `Strip path`: when matching, strip the matching prefix from the upstream request URL. Defaults to true\n* `Issue Let's Encrypt cert.`: automatically issue and renew let's encrypt certificate based on domain name. Only if Let's Encrypt enabled in global config.\n* `Issue certificate`: automatically issue and renew a certificate based on domain name\n* `Possible hostnames`: all the possible hostnames for your service\n* `Possible matching paths`: all the possible matching paths for your service\n\n### Redirection\n\n* `Redirection enabled`: enabled the redirection. If enabled, a call to that service will redirect to the chosen URL\n* `Http redirection code`: type of redirection used\n* `Redirect to`: URL used to redirect user when the service is called\n\n### Service targets\n\n* `Redirect to local`: if you work locally with Otoroshi, you may want to use that feature to redirect one specific service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests\n* `Load balancing`: the load balancing algorithm used\n* `Targets`: the list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n* `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is `/api/foo`, then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n\n### URL Patterns\n\n* `Make service a 'public ui'`: add a default pattern as public routes\n* `Make service a 'private api'`: add a default pattern as private routes\n* `Public patterns`: by default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use `/.*`\n* `Private patterns`: if you define a public pattern that is a little bit too much, you can make some of public URL private again\n\n### Restrictions\n\n* `Enabled`: enable restrictions\n* `Allow last`: Otoroshi will test forbidden and notFound paths before testing allowed paths\n* `Allowed`: allowed paths\n* `Forbidden`: forbidden paths\n* `Not Found`: not found paths\n\n### Otoroshi exchange protocol\n\n* `Enabled`: when enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside.\n* `Send challenge`: when disbaled, Otoroshi will not check if target service respond with sent random value.\n* `Send info. token`: when enabled, Otoroshi add an additional header containing current call informations\n* `Challenge token version`: version the otoroshi exchange protocol challenge. This option will be set to V2 in a near future.\n* `Info. token version`: version the otoroshi exchange protocol info token. This option will be set to Latest in a near future.\n* `Tokens TTL`: the number of seconds for tokens (state and info) lifes\n* `State token header name`: the name of the header containing the state token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.state)\n* `State token response header name`: the name of the header containing the state response token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.stateresp)\n* `Info token header name`: the name of the header containing the info token. If not specified, the value will be taken from the configuration (otoroshi.headers.comm.claim)\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Use same algo.`: when enabled, all JWT token in this section will use the same signing algorithm. If `use same algo.` is disabled, three more options will be displayed to select an algorithm for each step of the calls :\n * Otoroshi to backend\n * Backend to otoroshi\n * Info. token\n\n* `Algo.`: What kind of algorithm you want to use to verify/sign your JWT token with\n* `SHA Size`: Word size for the SHA-2 hash function used\n* `Hmac secret`: used to verify the token\n* `Base64 encoded secret`: if enabled, the extracted token will be base64 decoded before it is verifier\n\n### Authentication\n\n* `Enforce user authentication`: when enabled, user will be allowed to use the service (UI) only if they are registered users of the chosen authentication module.\n* `Auth. config`: authentication module used to protect the service\n* `Create a new auth config.`: navigate to the creation of authentication module page\n* `all auth config.`: navigate to the authentication pages\n\n* `Excluded patterns`: by default, when security is enabled, everything is secured. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n* `Strict mode`: strict mode enabled\n\n### Api keys constraints\n\n* `From basic auth.`: you can pass the api key in Authorization header (ie. from 'Authorization: Basic xxx' header)\n* `Allow client id only usage`: you can pass the api key using client id only (ie. from Otoroshi-Token header)\n* `From custom headers`: you can pass the api key using custom headers (ie. Otoroshi-Client-Id and Otoroshi-Client-Secret headers)\n* `From JWT token`: you can pass the api key using a JWT token (ie. from 'Authorization: Bearer xxx' header)\n\n#### Basic auth. Api Key\n\n* `Custom header name`: the name of the header to get Authorization\n* `Custom query param name`: the name of the query param to get Authorization\n\n#### Client ID only Api Key\n\n* `Custom header name`: the name of the header to get the client id\n* `Custom query param name`: the name of the query param to get the client id\n\n#### Custom headers Api Key\n\n* `Custom client id header name`: the name of the header to get the client id\n* `Custom client secret header name`: the name of the header to get the client secret\n\n#### JWT Token Api Key\n\n* `Secret signed`: JWT can be signed by apikey secret using HMAC algo.\n* `Keypair signed`: JWT can be signed by an otoroshi managed keypair using RSA/EC algo.\n* `Include Http request attrs.`: if enabled, you have to put the following fields in the JWT token corresponding to the current http call (httpPath, httpVerb, httpHost)\n* `Max accepted token lifetime`: the maximum number of second accepted as token lifespan\n* `Custom header name`: the name of the header to get the jwt token\n* `Custom query param name`: the name of the query param to get the jwt token\n* `Custom cookie name`: the name of the cookie to get the jwt token\n\n### Routing constraints\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata\n\n### CORS support\n\n* `Enabled`: if enabled, CORS header will be check for each incoming request\n* `Allow credentials`: if enabled, the credentials will be sent. Credentials are cookies, authorization headers, or TLS client certificates.\n* `Allow origin`: if enabled, it will indicates whether the response can be shared with requesting code from the given\n* `Max age`: response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.\n* `Expose headers`: response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.\n* `Allow headers`: response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.\n* `Allow methods`: response header specifies one or more methods allowed when accessing a resource in response to a preflight request.\n* `Excluded patterns`: by default, when cors is enabled, everything has cors. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n#### Related documentations\n\n* @link[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) { open=new }\n* @link[Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) { open=new }\n* @link[Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) { open=new }\n* @link[Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) { open=new }\n* @link[Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) { open=new }\n\n### JWT tokens verification\n\n* `Verifiers`: list of selected verifiers to apply on the service\n* `Enabled`: if enabled, Otoroshi will enabled each verifier of the previous list\n* `Excluded patterns`: list of routes where the verifiers will not be apply\n\n### Pre Routing\n\nThis part has been deprecated and moved to the plugin section.\n\n### Access validation\nThis part has been deprecated and moved to the plugin section.\n\n### Gzip support\n\n* `Mimetypes allowed list`: gzip only the files that are matching to a format in the list\n* `Mimetypes blocklist`: will not gzip files matching to a format in the list. A possible way is to allowed all format by default by setting a `*` on the `Mimetypes allowed list` and to add the unwanted format in this list.\n* `Compression level`: the compression level where 9 gives us maximum compression but at the slowest speed. The default compression level is 5 and is a good compromise between speed and compression ratio.\n* `Buffer size`: chunking up a stream of bytes into limited size\n* `Chunk threshold`: if the content type of a request reached over the threshold, the response will be chunked\n* `Excluded patterns`: by default, when gzip is enabled, everything has gzip. But sometimes you need to exlude something, so just add regex to matching path you want to exlude.\n\n### Client settings\n\n* `Use circuit breaker`: use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !\n* `Cache connections`: use a cache at host connection level to avoid reconnection time\n* `Client attempts`: specify how many times the client will retry to fetch the result of the request after an error before giving up.\n* `Client call timeout`: specify how long each call should last at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n* `C.breaker max errors`: specify how many errors can pass before opening the circuit breaker\n* `C.breaker retry delay`: specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor\n* `C.breaker backoff factor`: specify the factor to multiply the delay for each retry\n* `C.breaker window`: specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted\n\n#### Custom timeout settings (list)\n\n* `Path`: the path on which the timeout will be active\n* `Client connection timeout`: specify how long each connection should last at most in milliseconds.\n* `Client idle timeout`: specify how long each connection can stay in idle state at most in milliseconds.\n* `Client call and stream timeout`: specify how long each call should last at most in milliseconds for handling the request and streaming the response.\n* `Call timeout`: Specify how long each call should last at most in milliseconds.\n* `Client global timeout`: specify how long the global call (with retries) should last at most in milliseconds.\n\n#### Proxy settings\n\n* `Proxy host`: host of proxy behind the identify provider\n* `Proxy port`: port of proxy behind the identify provider\n* `Proxy principal`: user of proxy \n* `Proxy password`: password of proxy\n\n### HTTP Headers\n\n* `Additional Headers In`: specify headers that will be added to each client request (from Otoroshi to target). Useful to add authentication.\n* `Additional Headers Out`: specify headers that will be added to each client response (from Otoroshi to client).\n* `Missing only Headers In`: specify headers that will be added to each client request (from Otoroshi to target) if not in the original request.\n* `Missing only Headers Out`: specify headers that will be added to each client response (from Otoroshi to client) if not in the original response.\n* `Remove incoming headers`: remove headers in the client request (from client to Otoroshi).\n* `Remove outgoing headers`: remove headers in the client response (from Otoroshi to client).\n* `Security headers`:\n* `Utility headers`:\n* `Matching Headers`: specify headers that MUST be present on client request to route it (pre routing). Useful to implement versioning.\n* `Headers verification`: verify that some headers has a specific value (post routing)\n\n### Additional settings \n\n* `OpenAPI`: specify an open API descriptor. Useful to display the documentation\n* `Tags`: specify tags for the service\n* `Metadata`: specify metadata for the service. Useful for analytics\n* `IP allowed list`: IP address that can access the service\n* `IP blocklist`: IP address that cannot access the service\n\n### Canary mode\n\n* `Enabled`: Canary mode enabled\n* `Traffic split`: Ratio of traffic that will be sent to canary targets. For instance, if traffic is at 0.2, for 10 request, 2 request will go on canary targets and 8 will go on regular targets.\n* `Targets`: The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures\n * `Target`:\n * `Targets root`: Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar\n* `Campaign stats`:\n* `Use canary targets as standard targets`:\n\n### Healthcheck settings\n\n* `HealthCheck enabled`: to help failing fast, you can activate healthcheck on a specific URL.\n* `HealthCheck url`: the URL to check. Should return an HTTP 200 response. You can also respond with an 'Opun-Health-Check-Logic-Test-Result' header set to the value of the 'Opun-Health-Check-Logic-Test' request header + 42. to make the healthcheck complete.\n\n### Fault injection\n\n* `User facing app.`: if service is set as user facing, Snow Monkey can be configured to not being allowed to create outage on them.\n* `Chaos enabled`: activate or deactivate chaos setting on this service descriptor.\n\n### Custom errors template\n\n* `40x template`: html template displayed when 40x error occurred\n* `50x template`: html template displayed when 50x error occurred\n* `Build mode template`: html template displayed when the build mode is enabled\n* `Maintenance mode template`: html template displayed when the maintenance mode is enabled\n* `Custom messages`: override error message one by one\n\n### Request transformation\n\nThis part has been deprecated and moved to the plugin section.\n\n### Plugins\n\n* `Plugins`:\n \n * `Inject default config`: injects, if present, the default configuration of a selected plugin in the configuration object\n * `Documentation`: link to the documentation website of the plugin\n * `show/hide config. panel`: shows and hides the plugin panel which contains the plugin description and configuration\n* `Excluded patterns`: by default, when plugins are enabled, everything pass in. But sometimes you need to exclude something, so just add regex to matching path you want to exlude.\n* `Configuration`: the configuration of each enabled plugin, split by names and grouped in the same configuration object."},{"name":"service-groups.md","id":"/entities/service-groups.md","url":"/entities/service-groups.html","title":"Service groups","content":"# Service groups\n\nA service group is composed of an unique `id`, a `Group name`, a `Group description`, an `Organization` and a `Team`. As all Otoroshi resources, a service group have a list of tags and metadata associated.\n\n@@@ div { .centered-img }\n\n@@@\n\nThe first instinctive usage of service group is to group a list of services. \n\nWhen it's done, you can authorize an api key on a specific group. Instead of authorize an api key for each service, you can regroup a list of services together, and give authorization on the group (read the page on the api keys and the usage of the `Authorized on.` field).\n\n## Access to the list of service groups\n\nTo visualize and edit the list of groups, you can navigate to your instance on the `https://otoroshi.xxxxx/bo/dashboard/groups` route or click on the cog icon and select the Service groups button.\n\nOnce on the page, you can create a new item, edit an existing service group or delete an existing one.\n\n> When a service group is deleted, the resources associated are not deleted. On the other hand, the service group of associated resources is let empty.\n\n"},{"name":"tcp-services.md","id":"/entities/tcp-services.md","url":"/entities/tcp-services.html","title":"TCP services","content":"# TCP services\n\nTCP service are special kind of otoroshi services meant to proxy pure TCP connections (ssh, database, http, etc)\n\n## Global information\n\n* `Id`: generated unique identifier\n* `TCP service name`: the name of your TCP service\n* `Enabled`: enable and disable the service\n* `TCP service port`: the listening port\n* `TCP service interface`: network interface listen by the service\n* `Tags`: list of tags associated to the service\n* `Metadata`: list of metadata associated to the service\n\n## TLS\n\nthis section controls the TLS exposition of the service\n\n* `TLS mode`\n * `Disabled`: no TLS\n * `PassThrough`: as the target exposes TLS, the call will pass through otoroshi and use target TLS\n * `Enabled`: the service will be exposed using TLS and will chose certificate based on SNI\n* `Client Auth.`\n * `None` no mTLS needed to pass\n * `Want` pass with or without mTLS\n * `Need` need mTLS to pass\n\n## Server Name Indication (SNI)\n\nthis section control how SNI should be treated\n\n* `SNI routing enabled`: if enabled, the server will use the SNI hostname to determine which certificate to present to the client\n* `Forward to target if no SNI match`: if enabled, a call without any SNI match will be forward to the target\n* `Target host`: host of the target called if no SNI\n* `Target ip address`: ip of the target called if no SNI\n* `Target port`: port of the target called if no SNI\n* `TLS call`: encrypt the communication with TLS\n\n## Rules\n\nfor any listening TCP proxy, it is possible to route to multiple targets based on SNI or extracted http host (if proxying http)\n\n* `Matching domain name`: regex used to filter the list of domains where the rule will be applied\n* `Target host`: host of the target\n* `Target ip address`: ip of the target\n* `Target port`: port of the target\n* `TLS call`: enable this flag if the target is exposed using TLS\n"},{"name":"teams.md","id":"/entities/teams.md","url":"/entities/teams.html","title":"Teams","content":"# Teams\n\nIn Otoroshi, all resources are attached to an `Organization` and a `Team`. \n\nA team is composed of an unique `id`, a `name`, a `description` and an `Organization`. As all Otoroshi resources, a Team have a list of tags and metadata associated.\n\nA team have an unique organization and can be use on multiples resources (services, api keys, etc ...).\n\nA connected user on Otoroshi UI has a list of teams and organizations associated. It can be helpful when you want restrict the rights of a connected user.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Access to the list of teams\n\nTo visualize and edit the list of teams, you can navigate to your instance on the `https://otoroshi.xxxxxx/bo/dashboard/teams` route or click on the cog icon and select the teams button.\n\nOnce on the page, you can create a new item, edit an existing team or delete an existing one.\n\n> When a team is deleted, the resources associated are not deleted. On the other hand, the team of associated resources is let empty.\n\n## Entities location\n\nAny otoroshi entity has a location property (`_loc` when serialized to json) explaining where and by whom the entity can be seen. \n\nAn entity can be part of multiple teams in an organization\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"team-1\",\n \"team-2\"\n ]\n }\n ...\n}\n```\n\nor all teams\n\n```javascript\n{\n \"_loc\": {\n \"tenant\": \"tenant-1\",\n \"teams\": [\n \"*\"\n ]\n }\n ...\n}\n```"},{"name":"features.md","id":"/features.md","url":"/features.html","title":"Features","content":"# Features\n\n**Traffic Management**\n\n* Can proxy any HTTP(s) service (apis, webapps, websocket, etc)\n* Can proxy any TCP service (app, database, etc)\n* Can proxy any GRPC service\n* Multiple load-balancing options: \n * RoundRobin\n * Random, Sticky\n * Ip address hash\n * Best Response Time\n* Distributed in-flight request limiting\t\n* Distributed rate limiting \n* End-to-end HTTP/1.1 support\n* End-to-end H2 support\n* End-to-end H3 support\n* Traffic mirroring\n* Traffic capture\n* Canary deployments\n* Relay routing \n* Tunnels for easier network exposition\n* Error templates\n\n**Routing**\n\n* Router can support ten of thousands of concurrent routes\n* Router support path params extraction (can be regex validated)\n* Routing based on \n * method\n * hostname (exact, wildcard)\n * path (exact, wildcard)\n * header values (exact, regex, wildcard)\n * query param values (exact, regex, wildcard)\n* Support full url rewriting\n\n**Routes customization**\n\n* Dozens of built-in middlewares (policies/plugins) \n * circuit breakers\n * automatic retries\n * buffering\n * gzip\n * headers manipulation\n * cors\n * body transformation\n * graphql gateway\n * etc \n* Support middlewares compiled to WASM (using extism)\n* Support Open Policy Agent policies for traffic control\n* Write your own custom middlewares\n * in scala deployed as jar files\n * in whatever language you want that can be compiled to WASM\n\n**Routes Monitoring**\n\n* Active healthchecks\n* Route state for the last 90 days\n* Calls tracing using W3C trace context\n* Export alerts and events to external database\n * file\n * S3\n * elastic\n * pulsar\n * kafka\n * webhook\n * mailer\n * logger\n* Real-time traffic metrics\n* Real-time traffic metrics (Datadog, Prometheus, StatsD)\n\n**Services discovery**\n\n* through DNS\n* through Eureka 2\n* through Kubernetes API\n* through custom otoroshi protocol\n\n**API security**\n\n* Access management with apikeys and quotas\n* Automatic apikeys secrets rotation\n* HTTPS and TLS\n* End-to-end mTLS calls \n* Routing constraints\n* Routing restrictions\n* JWT tokens validation and manipulation\n * can support multiple validator on the same routes\n\n**Administration UI**\n\n* Manage and organize all resources\n* Secured users access with Authentication module\n* Audited users actions\n* Dynamic changes at runtime without full reload\n* Test your routes without any external tools\n\n**Webapp authentication and security**\n\n* OAuth2.0/2.1 authentication\n* OpenID Connect (OIDC) authentication\n* LDAP authentication\n* JWT authentication\n* OAuth 1.0a authentication\n* SAML V2 authentication\n* Internal users management\n* Secret vaults support\n * Environment variables\n * Hashicorp Vault\n * Azure key vault\n * AWS secret manager\n * Google secret manager\n * Kubernetes secrets\n * Izanami\n * Spring Cloud Config\n * Http\n * Local\n\n**Certificates management**\n\n* Dynamic TLS certificates store \n* Dynamic TLS termination\n* Internal PKI\n * generate self signed certificates/CAs\n * generate/sign certificates/CAs/subCAs\n * AIA\n * OCSP responder\n * import P12/certificate bundles\n* ACME / Let's Encrypt support\n* On-the-fly certificate generation based on a CA certificate without request loss\n* JWKS exposition for public keypair\n* Default certificate\n* Customize mTLS trusted CAs in the TLS handshake\n\n**Clustering**\n\n* based on a control plane/data plane pattern\n* encrypted communication\n* backup capabilities to allow data plane to start without control plane reachable to improve resilience\n* relay routing to forward traffic from one network zone to others\n* distributed web authentication accross nodes\n\n**Performances and testing**\n\n* Chaos engineering\n* Horizontal Scalability or clustering\n* Canary testing\n* Http client in UI\n* Request debugging\n* Traffic capture\n\n**Kubernetes integration**\n\n* Standard Ingress controller\n* Custom Ingress controller\n * Manage Otoroshi resources from Kubernetes\n* Validation of resources via webhook\n* Service Mesh for easy service-to-service communication (based on Kubernetes sidecars)\n\n**Organize**\n\n* multi-organizations\n* multi-teams\n* routes groups\n\n**Developpers portal**\n\n* Using @link:[Daikoku](https://maif.github.io/daikoku/manual/index.html) { open=new }\n"},{"name":"getting-started.md","id":"/getting-started.md","url":"/getting-started.html","title":"Getting Started","content":"# Getting Started\n\n- [Protect your service with Otoroshi ApiKey](#protect-your-service-with-otoroshi-apikey)\n- [Secure your web app in 2 calls with an authentication](#secure-your-web-app-in-2-calls-with-an-authentication)\n\nDownload the latest jar of Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nOnce downloading, run Otoroshi.\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nYes, that command is all it took to start it up.\n\n## Protect your service with Otoroshi ApiKey\n\n
      \nRoute plugins:\nApikeys\n
      \n\nCreate a new route, exposed on `http://myapi.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"enabled\": true,\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNow that we have created our route, let’s see if our request reaches our upstream service. \nYou should receive an error from Otoroshi about a missing api key in our request.\n\n```sh\ncurl 'http://myapi.oto.tools:8080'\n```\n\nIt looks like we don’t have access to it. Create your first api key with a quota of 10 calls by day and month.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"my-first-apikey-id\",\n \"clientSecret\": \"my-first-apikey-secret\",\n \"clientName\": \"my-first-apikey\",\n \"description\": \"my-first-apikey-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10\n}\nEOF\n```\n\nCall your api with the generated apikey.\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret\n```\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"authorization\": \"Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==\",\n \"otoroshi-request-id\": \"1465298507974836306\",\n \"otoroshi-proxied-host\": \"myapi.oto.tools:8080\",\n \"otoroshi-request-timestamp\": \"2021-11-29T13:36:02.888+01:00\",\n },\n \"body\": \"\"\n}\n```\n\nCheck your remaining quotas\n\n```sh\ncurl 'http://myapi.oto.tools:8080' -u my-first-apikey-id:my-first-apikey-secret --include\n```\n\nThis should output these following Otoroshi headers\n\n```json\nOtoroshi-Daily-Calls-Remaining: 6\nOtoroshi-Monthly-Calls-Remaining: 6\n```\n\nKeep calling the api and confirm that Otoroshi is sending you an apikey exceeding quota error\n\n\n```json\n{ \n \"Otoroshi-Error\": \"You performed too much requests\"\n}\n```\n\nWell done, you have secured your first api with the apikeys system with limited call quotas.\n\n## Secure your web app in 2 calls with an authentication\n\n
      \nRoute plugins:\nAuthentication\n
      \n\nCreate an in-memory authentication module, with one registered user, to protect your service.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/auths' \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"type\":\"basic\",\n \"id\":\"auth_mod_in_memory_auth\",\n \"name\":\"in-memory-auth\",\n \"desc\":\"in-memory-auth\",\n \"users\":[\n {\n \"name\":\"User Otoroshi\",\n \"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\":\"user@foo.bar\",\n \"metadata\":{\n \"username\":\"roger\"\n },\n \"tags\":[\"foo\"],\n \"webauthn\":null,\n \"rights\":[{\n \"tenant\":\"*:r\",\n \"teams\":[\"*:r\"]\n }]\n }\n ],\n \"sessionCookieValues\":{\n \"httpOnly\":true,\n \"secure\":false\n }\n}\nEOF\n```\n\nThen create a service secure by the previous authentication module, which proxies `google.fr` on `webapp.oto.tools`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"google.fr\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"enabled\": true,\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to http://webapp.oto.tools:8080, login with `user@foo.bar/password` and check that you're redirect to `google` page.\n\nWell done! You completed the discovery tutorial."},{"name":"communicate-with-kafka.md","id":"/how-to-s/communicate-with-kafka.md","url":"/how-to-s/communicate-with-kafka.html","title":"Communicate with Kafka","content":"# Communicate with Kafka\n\nEvery matching event can be sent to an [Apache Kafka topic](https://kafka.apache.org/).\n\n### SASL mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:SASL_PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n - KAFKA_CLIENT_USERS=user\n - KAFKA_CLIENT_PASSWORDS=password\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### PLAINTEXT mechanism\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: \"2\"\n\nservices:\n zookeeper:\n image: docker.io/bitnami/zookeeper:3.8\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n kafka:\n image: docker.io/bitnami/kafka:3.2\n ports:\n - \"9092:9092\"\n environment:\n - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181\n - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT\n - ALLOW_PLAINTEXT_LISTENER=yes\n - KAFKA_CFG_LISTENERS=INTERNAL://:9093,CLIENT://:9092\n - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9093,CLIENT://kafka:9092\n - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL\n\n depends_on:\n - zookeeper\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SSL mechanism\n\n````bash\nwget https://raw.githubusercontent.com/confluentinc/confluent-platform-security-tools/master/kafka-generate-ssl.sh\n````\n\n````bash\nchmod +x kafka-generate-ssl.sh\n````\n\nCreate a `docker-compose.yml` with the following content\n\n````yml\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"wurstmeister/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n\n kafka:\n image: wurstmeister/kafka:2.12-2.2.0\n depends_on:\n - zookeeper\n ports:\n - \"9092:9092\"\n environment:\n KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:9092'\n KAFKA_LISTENERS: 'SSL://0.0.0.0:9092'\n KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'\n KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_SSL_KEYSTORE_LOCATION: '/keystore/kafka.keystore.jks'\n KAFKA_SSL_KEYSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_KEY_PASSWORD: 'otoroshi'\n KAFKA_SSL_TRUSTSTORE_LOCATION: '/truststore/kafka.truststore.jks'\n KAFKA_SSL_TRUSTSTORE_PASSWORD: 'otoroshi'\n KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''\n KAFKA_SECURITY_INTER_BROKER_PROTOCOL: 'SSL'\n volumes:\n - ./truststore:/truststore\n - ./keystore:/keystore\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n\n### SASL_SSL mechanism\n\nGenerate the TLS certificates for the Kafka broker.\n\nCreate a file `generate.sh` with the following content and run the command\n\n````bash\nchmod +x generate.sh && ./generate.sh\n````\n\n````bash\n# Content of the generate.sh file\n\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚°\n 79966b@PMP00131 î‚° ~/Downloads/kafka_ssl_setup-master î‚° cat generate.sh\n#!/usr/bin/env bash\n\nset -e\n\nKEYSTORE_FILENAME=\"kafka.keystore.jks\"\nVALIDITY_IN_DAYS=3650\nDEFAULT_TRUSTSTORE_FILENAME=\"kafka.truststore.jks\"\nTRUSTSTORE_WORKING_DIRECTORY=\"truststore\"\nKEYSTORE_WORKING_DIRECTORY=\"keystore\"\nCA_CERT_FILE=\"ca-cert\"\nKEYSTORE_SIGN_REQUEST=\"cert-file\"\nKEYSTORE_SIGN_REQUEST_SRL=\"ca-cert.srl\"\nKEYSTORE_SIGNED_CERT=\"cert-signed\"\n\nfunction file_exists_and_exit() {\n echo \"'$1' cannot exist. Move or delete it before\"\n echo \"re-running this script.\"\n exit 1\n}\n\nif [ -e \"$KEYSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $KEYSTORE_WORKING_DIRECTORY\nfi\n\nif [ -e \"$CA_CERT_FILE\" ]; then\n file_exists_and_exit $CA_CERT_FILE\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST_SRL\" ]; then\n file_exists_and_exit $KEYSTORE_SIGN_REQUEST_SRL\nfi\n\nif [ -e \"$KEYSTORE_SIGNED_CERT\" ]; then\n file_exists_and_exit $KEYSTORE_SIGNED_CERT\nfi\n\necho\necho \"Welcome to the Kafka SSL keystore and truststore generator script.\"\n\necho\necho \"First, do you need to generate a trust store and associated private key,\"\necho \"or do you already have a trust store file and private key?\"\necho\necho -n \"Do you need to generate a trust store and associated private key? [yn] \"\nread generate_trust_store\n\ntrust_store_file=\"\"\ntrust_store_private_key_file=\"\"\n\nif [ \"$generate_trust_store\" == \"y\" ]; then\n if [ -e \"$TRUSTSTORE_WORKING_DIRECTORY\" ]; then\n file_exists_and_exit $TRUSTSTORE_WORKING_DIRECTORY\n fi\n\n mkdir $TRUSTSTORE_WORKING_DIRECTORY\n echo\n echo \"OK, we'll generate a trust store and associated private key.\"\n echo\n echo \"First, the private key.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - A password for the private key. Remember this.\"\n echo \" - Information about you and your company.\"\n echo \" - NOTE that the Common Name (CN) is currently not important.\"\n\n openssl req -new -x509 -keyout $TRUSTSTORE_WORKING_DIRECTORY/ca-key \\\n -out $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -days $VALIDITY_IN_DAYS\n\n trust_store_private_key_file=\"$TRUSTSTORE_WORKING_DIRECTORY/ca-key\"\n\n echo\n echo \"Two files were created:\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-key -- the private key used later to\"\n echo \" sign certificates\"\n echo \" - $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE -- the certificate that will be\"\n echo \" stored in the trust store in a moment and serve as the certificate\"\n echo \" authority (CA). Once this certificate has been stored in the trust\"\n echo \" store, it will be deleted. It can be retrieved from the trust store via:\"\n echo \" $ keytool -keystore -export -alias CARoot -rfc\"\n\n echo\n echo \"Now the trust store will be generated from the certificate.\"\n echo\n echo \"You will be prompted for:\"\n echo \" - the trust store's password (labeled 'keystore'). Remember this\"\n echo \" - a confirmation that you want to import the certificate\"\n\n keytool -keystore $TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME \\\n -alias CARoot -import -file $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\n\n trust_store_file=\"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME\"\n\n echo\n echo \"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME was created.\"\n\n # don't need the cert because it's in the trust store.\n rm $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\nelse\n echo\n echo -n \"Enter the path of the trust store file. \"\n read -e trust_store_file\n\n if ! [ -f $trust_store_file ]; then\n echo \"$trust_store_file isn't a file. Exiting.\"\n exit 1\n fi\n\n echo -n \"Enter the path of the trust store's private key. \"\n read -e trust_store_private_key_file\n\n if ! [ -f $trust_store_private_key_file ]; then\n echo \"$trust_store_private_key_file isn't a file. Exiting.\"\n exit 1\n fi\nfi\n\necho\necho \"Continuing with:\"\necho \" - trust store file: $trust_store_file\"\necho \" - trust store private key: $trust_store_private_key_file\"\n\nmkdir $KEYSTORE_WORKING_DIRECTORY\n\necho\necho \"Now, a keystore will be generated. Each broker and logical client needs its own\"\necho \"keystore. This script will create only one keystore. Run this script multiple\"\necho \"times for multiple keystores.\"\necho\necho \"You will be prompted for the following:\"\necho \" - A keystore password. Remember it.\"\necho \" - Personal information, such as your name.\"\necho \" NOTE: currently in Kafka, the Common Name (CN) does not need to be the FQDN of\"\necho \" this host. However, at some point, this may change. As such, make the CN\"\necho \" the FQDN. Some operating systems call the CN prompt 'first / last name'\"\necho \" - A key password, for the key being generated within the keystore. Remember this.\"\n\n# To learn more about CNs and FQDNs, read:\n# https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedTrustManager.html\n\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME \\\n -alias localhost -validity $VALIDITY_IN_DAYS -genkey -keyalg RSA\n\necho\necho \"'$KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME' now contains a key pair and a\"\necho \"self-signed certificate. Again, this keystore can only be used for one broker or\"\necho \"one logical client. Other brokers or clients need to generate their own keystores.\"\n\necho\necho \"Fetching the certificate from the trust store and storing in $CA_CERT_FILE.\"\necho\necho \"You will be prompted for the trust store's password (labeled 'keystore')\"\n\nkeytool -keystore $trust_store_file -export -alias CARoot -rfc -file $CA_CERT_FILE\n\necho\necho \"Now a certificate signing request will be made to the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost \\\n -certreq -file $KEYSTORE_SIGN_REQUEST\n\necho\necho \"Now the trust store's private key (CA) will sign the keystore's certificate.\"\necho\necho \"You will be prompted for the trust store's private key password.\"\nopenssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \\\n -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \\\n -days $VALIDITY_IN_DAYS -CAcreateserial\n# creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed.\n\necho\necho \"Now the CA will be imported into the keystore.\"\necho\necho \"You will be prompted for the keystore's password and a confirmation that you want to\"\necho \"import the certificate.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias CARoot \\\n -import -file $CA_CERT_FILE\nrm $CA_CERT_FILE # delete the trust store cert because it's stored in the trust store.\n\necho\necho \"Now the keystore's signed certificate will be imported back into the keystore.\"\necho\necho \"You will be prompted for the keystore's password.\"\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost -import \\\n -file $KEYSTORE_SIGNED_CERT\n\necho\necho \"All done!\"\necho\necho \"Delete intermediate files? They are:\"\necho \" - '$KEYSTORE_SIGN_REQUEST_SRL': CA serial number\"\necho \" - '$KEYSTORE_SIGN_REQUEST': the keystore's certificate signing request\"\necho \" (that was fulfilled)\"\necho \" - '$KEYSTORE_SIGNED_CERT': the keystore's certificate, signed by the CA, and stored back\"\necho \" into the keystore\"\necho -n \"Delete? [yn] \"\nread delete_intermediate_files\n\nif [ \"$delete_intermediate_files\" == \"y\" ]; then\n rm $KEYSTORE_SIGN_REQUEST_SRL\n rm $KEYSTORE_SIGN_REQUEST\n rm $KEYSTORE_SIGNED_CERT\nfi\n````\n\nCreate, in the same repository, a repository named `secrets` with the following configuration.\n\n````bash \n# Content of ~/tmp/kafka/secrets/kafka_server_jaas.conf\n\nClient {\n org.apache.kafka.common.security.plain.PlainLoginModule required\n username=\"user\"\n password=\"password\";\n};\n````\n\nCreate a `docker-compose.yml` file with the following content.\n\n````bash\nversion: '3.5'\n\nservices:\n\n zookeeper:\n image: \"bitnami/zookeeper:latest\"\n ports:\n - \"2181:2181\"\n environment:\n - ALLOW_ANONYMOUS_LOGIN=yes\n\n kafka:\n image: bitnami/kafka:latest\n depends_on:\n - zookeeper\n ports:\n - '9092:9092'\n environment:\n ALLOW_PLAINTEXT_LISTENER: 'yes'\n KAFKA_ZOOKEEPER_PROTOCOL: 'PLAINTEXT'\n KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CLIENT:SASL_SSL'\n KAFKA_CFG_LISTENERS: 'INTERNAL://:9093,CLIENT://:9092'\n KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'\n KAFKA_CFG_ADVERTISED_LISTENERS: 'INTERNAL://kafka:9093,CLIENT://kafka:9092'\n KAFKA_CLIENT_USERS: 'user'\n KAFKA_CLIENT_PASSWORDS: 'password'\n KAFKA_CERTIFICATE_PASSWORD: 'otoroshi'\n KAFKA_TLS_TYPE: 'JKS'\n KAFKA_OPTS: \"-Djava.security.auth.login.config=/opt/kafka/kafka_server_jaas.conf\"\n volumes:\n - ./secrets/kafka_server_jaas.conf:/opt/kafka/kafka_server_jaas.conf\n - ./truststore/kafka.truststore.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n - ./keystore/kafka.keystore.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n````\n\nAt this point, your repository should be \n````\n/tmp/kafka\n | generate.sh\n | docker-compose.yml\n | truststore\n | kafka.truststore.jks\n | keystore \n | kafka.keystore.jks\n | secrets \n | kafka_server_jaas.conf\n````\n\nLaunch the command to create the zookeeper and kafka containers\n\n````bash\ndocker-compose up -d\n````\n\nCreate a new exporter on your Otoroshi instance with the following values\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"create-custom-auth-module.md","id":"/how-to-s/create-custom-auth-module.md","url":"/how-to-s/create-custom-auth-module.html","title":"Create your Authentication module","content":"# Create your Authentication module\n\nAuthentication modules can be used to protect routes. In some cases, you need to create your own custom authentication module to create a new one or simply inherit and extend an exiting module.\n\nYou can write your own authentication using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"my-custom-auth-module\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1x.x.x\"\n )\n```\n\nJust below, you can find an example of Custom Auth. module. \n\n```scala\npackage auth.custom\n\nimport akka.http.scaladsl.util.FastFuture\nimport otoroshi.auth.{AuthModule, AuthModuleConfig, Form, SessionCookieValues}\nimport otoroshi.controllers.routes\nimport otoroshi.env.Env\nimport otoroshi.models._\nimport otoroshi.security.IdGenerator\nimport otoroshi.utils.JsonPathValidator\nimport otoroshi.utils.syntax.implicits.BetterSyntax\nimport play.api.http.MimeTypes\nimport play.api.libs.json._\nimport play.api.mvc._\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\ncase class CustomModuleConfig(\n id: String,\n name: String,\n desc: String,\n clientSideSessionEnabled: Boolean,\n sessionMaxAge: Int = 86400,\n userValidators: Seq[JsonPathValidator] = Seq.empty,\n tags: Seq[String],\n metadata: Map[String, String],\n sessionCookieValues: SessionCookieValues,\n location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),\n form: Option[Form] = None,\n foo: String = \"bar\"\n ) extends AuthModuleConfig {\n def `type`: String = \"custom\"\n def humanName: String = \"Custom Authentication\"\n\n override def authModule(config: GlobalConfig): AuthModule = CustomAuthModule(this)\n override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)\n\n lazy val format = new Format[CustomModuleConfig] {\n override def writes(o: CustomModuleConfig): JsValue = o.asJson\n\n override def reads(json: JsValue): JsResult[CustomModuleConfig] = Try {\n CustomModuleConfig(\n location = otoroshi.models.EntityLocation.readFromKey(json),\n id = (json \\ \"id\").as[String],\n name = (json \\ \"name\").as[String],\n desc = (json \\ \"desc\").asOpt[String].getOrElse(\"--\"),\n clientSideSessionEnabled = (json \\ \"clientSideSessionEnabled\").asOpt[Boolean].getOrElse(true),\n sessionMaxAge = (json \\ \"sessionMaxAge\").asOpt[Int].getOrElse(86400),\n metadata = (json \\ \"metadata\").asOpt[Map[String, String]].getOrElse(Map.empty),\n tags = (json \\ \"tags\").asOpt[Seq[String]].getOrElse(Seq.empty[String]),\n sessionCookieValues =\n (json \\ \"sessionCookieValues\").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),\n userValidators = (json \\ \"userValidators\")\n .asOpt[Seq[JsValue]]\n .map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))\n .getOrElse(Seq.empty),\n form = (json \\ \"form\").asOpt[JsValue].flatMap(json => Form._fmt.reads(json) match {\n case JsSuccess(value, _) => Some(value)\n case JsError(_) => None\n }),\n foo = (json \\ \"foo\").asOpt[String].getOrElse(\"bar\")\n )\n } match {\n case Failure(exception) => JsError(exception.getMessage)\n case Success(value) => JsSuccess(value)\n }\n }.asInstanceOf[Format[AuthModuleConfig]]\n\n override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = format\n\n override def asJson =\n location.jsonWithKey ++ Json.obj(\n \"type\" -> \"custom\",\n \"id\" -> this.id,\n \"name\" -> this.name,\n \"desc\" -> this.desc,\n \"clientSideSessionEnabled\" -> this.clientSideSessionEnabled,\n \"sessionMaxAge\" -> this.sessionMaxAge,\n \"metadata\" -> this.metadata,\n \"tags\" -> JsArray(tags.map(JsString.apply)),\n \"sessionCookieValues\" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),\n \"userValidators\" -> JsArray(userValidators.map(_.json)),\n \"form\" -> this.form.map(Form._fmt.writes),\n \"foo\" -> foo\n )\n\n def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)\n\n override def cookieSuffix(desc: ServiceDescriptor) = s\"custom-auth-$id\"\n def theDescription: String = desc\n def theMetadata: Map[String, String] = metadata\n def theName: String = name\n def theTags: Seq[String] = tags\n}\n\nobject CustomAuthModule {\n def defaultConfig = CustomModuleConfig(\n id = IdGenerator.namedId(\"auth_mod\", IdGenerator.uuid),\n name = \"My custom auth. module\",\n desc = \"My custom auth. module\",\n tags = Seq.empty,\n metadata = Map.empty,\n sessionCookieValues = SessionCookieValues(),\n clientSideSessionEnabled = true,\n form = None)\n}\n\ncase class CustomAuthModule(authConfig: CustomModuleConfig) extends AuthModule {\n def this() = this(CustomAuthModule.defaultConfig)\n\n override def paLoginPage(request: RequestHeader, config: GlobalConfig, descriptor: ServiceDescriptor, isRoute: Boolean)\n (implicit ec: ExecutionContext, env: Env): Future[Result] = {\n val redirect = request.getQueryString(\"redirect\")\n val hash = env.sign(s\"${authConfig.id}:::${descriptor.id}\")\n env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>\n Results\n .Ok(auth.custom.views.html.login(s\"/privateapps/generic/callback?desc=${descriptor.id}&hash=$hash&route=${isRoute}\", token))\n .as(MimeTypes.HTML)\n .addingToSession(\n \"ref\" -> authConfig.id,\n s\"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}\" -> redirect.getOrElse(\n routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)(request)\n )\n )(request)\n .future\n }\n }\n\n override def paLogout(request: RequestHeader, user: Option[PrivateAppsUser], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = FastFuture.successful(Right(None))\n\n override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)\n (implicit ec: ExecutionContext, env: Env): Future[Either[String, PrivateAppsUser]] = {\n PrivateAppsUser(\n randomId = IdGenerator.token(64),\n name = \"foo\",\n email = s\"foo@oto.tools\",\n profile = Json.obj(\n \"name\" -> \"foo\",\n \"email\" -> s\"foo@oto.tools\"\n ),\n realm = authConfig.cookieSuffix(descriptor),\n otoroshiData = None,\n authConfigId = authConfig.id,\n tags = Seq.empty,\n metadata = Map.empty,\n location = authConfig.location\n )\n .validate(authConfig.userValidators)\n .vfuture\n }\n\n override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Result] = ???\n\n override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Option[String]]] = ???\n\n override def boCallback(request: Request[AnyContent], config: GlobalConfig)(implicit ec: ExecutionContext, env: Env): Future[Either[String, BackOfficeUser]] = ???\n}\n```\n\nThis custom Auth. module inherits from AuthModule (the Auth module have to inherit from the AuthModule trait to be found by Otoroshi). It exposes a simple UI to login, and create an user for each callback request without any verification. Methods starting with bo will be called in case that the auth. module is used on the back office and in other cases, the pa methods (pa for Private App) will be called to protect a route.\n\nThis custom Auth. module uses a [Play template](https://www.playframework.com/documentation/2.8.x/ScalaTemplates) to display the login page. It's not required by Otoroshi but it's a easy way to create a login form.\n\n```html \n@import otoroshi.env.Env\n\n@(action: String, token: String)\n\n
      \n

      Login page

      \n\n
      \n \n \n Login\n \n \n
      \n```\n\nYour hierarchy files should be something like:\n\n```\nauth\n| custom\n |customModule.scala\n | views\n | login.scala.html\n```\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/customModule.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in the authentication modules, you can chose your custom module in the list."},{"name":"custom-initial-state.md","id":"/how-to-s/custom-initial-state.md","url":"/how-to-s/custom-initial-state.html","title":"Initial state customization","content":"# Initial state customization\n\nwhen you start otoroshi for the first time, some basic entities will be created and stored in the datastore in order to make your instance work properly. However it might not be enough for your use case but you do want to bother with restoring a complete otoroshi export.\n\nIn order to make state customization easy, otoroshi provides the config. key `otoroshi.initialCustomization`, overriden by the env. variable `OTOROSHI_INITIAL_CUSTOMIZATION`\n\nThe expected structure is the following :\n\n```javascript\n{\n \"config\": { ... },\n \"admins\": [],\n \"simpleAdmins\": [],\n \"serviceGroups\": [],\n \"apiKeys\": [],\n \"serviceDescriptors\": [],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [],\n \"teams\": []\n}\n```\n\nin this structure, everything is optional. For every array property, items will be added to the datastore. For the global config. object, you can just add the parts that you need, and they will be merged with the existing config. object of the datastore.\n\n## Customize the global config.\n\nfor instance, if you want to customize the behavior of the TLS termination, you can use the following :\n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"config\":{\"tlsSettings\":{\"defaultDomain\":\"www.foo.bar\",\"randomIfNotFound\":false}}'\n```\n\n## Customize entities\n\nif you want to add apikeys at first boot \n\n```sh\nexport OTOROSHI_INITIAL_CUSTOMIZATION='{\"apikeys\":[{\"_loc\":{\"tenant\":\"default\",\"teams\":[\"default\"]},\"clientId\":\"ksVlQ2KlZm0CnDfP\",\"clientSecret\":\"usZYbE1iwSsbpKY45W8kdbZySj1M5CWvFXe0sPbZ0glw6JalMsgorDvSBdr2ZVBk\",\"clientName\":\"awesome-apikey\",\"description\":\"the awesome apikey\",\"authorizedGroup\":\"default\",\"authorizedEntities\":[\"group_default\"],\"enabled\":true,\"readOnly\":false,\"allowClientIdOnly\":false,\"throttlingQuota\":10000000,\"dailyQuota\":10000000,\"monthlyQuota\":10000000,\"constrainedServicesOnly\":false,\"restrictions\":{\"enabled\":false,\"allowLast\":true,\"allowed\":[],\"forbidden\":[],\"notFound\":[]},\"rotation\":{\"enabled\":false,\"rotationEvery\":744,\"gracePeriod\":168,\"nextSecret\":null},\"validUntil\":null,\"tags\":[],\"metadata\":{}}]}'\n```\n"},{"name":"custom-log-levels.md","id":"/how-to-s/custom-log-levels.md","url":"/how-to-s/custom-log-levels.html","title":"Log levels customization","content":"# Log levels customization\n\nIf you want to customize the log level of your otoroshi instances, it's pretty easy to do it using environment variables or configuration file.\n\n## Customize log level for one logger with configuration file\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare in your otoroshi configuration file\n\n```\notoroshi.loggers {\n ...\n otoroshi-http-handler = \"DEBUG\"\n ...\n}\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## Customize log level for one logger with environment variable\n\nLet say you want to see `DEBUG` messages from the logger `otoroshi-http-handler`.\n\nThen you just have to declare an environment variable named `OTOROSHI_LOGGERS_OTOROSHI_HTTP_HANDLER` with value `DEBUG`. The rule is \n\n```scala\n\"OTOROSHI_LOGGERS_\" + loggerName.toUpperCase().replace(\"-\", \"_\")\n```\n\npossible levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default one is `WARN`.\n\n## List of loggers\n\n* [`otoroshi-error-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-handler%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-http-handler-debug`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler-debug%22%29)\n* [`otoroshi-websocket-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler%22%29)\n* [`otoroshi-websocket`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket%22%29)\n* [`otoroshi-websocket-handler-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-websocket-handler-actor%22%29)\n* [`otoroshi-snowmonkey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-circuit-breaker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-circuit-breaker%22%29)\n* [`otoroshi-worker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-worker%22%29)\n* [`otoroshi-http-handler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-handler%22%29)\n* [`otoroshi-auth-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-controller%22%29)\n* [`otoroshi-swagger-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-swagger-controller%22%29)\n* [`otoroshi-u2f-controller`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-u2f-controller%22%29)\n* [`otoroshi-backoffice-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-backoffice-api%22%29)\n* [`otoroshi-health-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-api%22%29)\n* [`otoroshi-stats-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-stats-api%22%29)\n* [`otoroshi-admin-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-admin-api%22%29)\n* [`otoroshi-auth-modules-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-modules-api%22%29)\n* [`otoroshi-certificates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-api%22%29)\n* [`otoroshi-pki`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-pki%22%29)\n* [`otoroshi-scripts-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-scripts-api%22%29)\n* [`otoroshi-analytics-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-api%22%29)\n* [`otoroshi-import-export-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-import-export-api%22%29)\n* [`otoroshi-templates-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-templates-api%22%29)\n* [`otoroshi-teams-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-teams-api%22%29)\n* [`otoroshi-events-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-api%22%29)\n* [`otoroshi-canary-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary-api%22%29)\n* [`otoroshi-data-exporter-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-api%22%29)\n* [`otoroshi-services-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-services-api%22%29)\n* [`otoroshi-tcp-service-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-service-api%22%29)\n* [`otoroshi-tenants-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tenants-api%22%29)\n* [`otoroshi-global-config-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config-api%22%29)\n* [`otoroshi-apikeys-fs-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fs-api%22%29)\n* [`otoroshi-apikeys-fg-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-fg-api%22%29)\n* [`otoroshi-apikeys-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-api%22%29)\n* [`otoroshi-statsd-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-statsd-actor%22%29)\n* [`otoroshi-snow-monkey-api`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snow-monkey-api%22%29)\n* [`otoroshi-jobs-eventstore-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-eventstore-checker%22%29)\n* [`otoroshi-initials-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-initials-certs-job%22%29)\n* [`otoroshi-alert-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor%22%29)\n* [`otoroshi-alert-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alert-actor-supervizer%22%29)\n* [`otoroshi-alerts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-alerts%22%29)\n* [`otoroshi-apikeys-secrets-rotation-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-secrets-rotation-job%22%29)\n* [`otoroshi-loader`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-loader%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-api-action`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-api-action%22%29)\n* [`otoroshi-analytics-writes-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-writes-elastic%22%29)\n* [`otoroshi-analytics-reads-elastic`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-reads-elastic%22%29)\n* [`otoroshi-events-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-events-actor-supervizer%22%29)\n* [`otoroshi-data-exporter`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter%22%29)\n* [`otoroshi-data-exporter-update-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-update-job%22%29)\n* [`otoroshi-kafka-wrapper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-wrapper%22%29)\n* [`otoroshi-kafka-connector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-kafka-connector%22%29)\n* [`otoroshi-analytics-webhook`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-webhook%22%29)\n* [`otoroshi-jobs-software-updates`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jobs-software-updates%22%29)\n* [`otoroshi-analytics-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor%22%29)\n* [`otoroshi-analytics-actor-supervizer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-actor-supervizer%22%29)\n* [`otoroshi-analytics-event`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-analytics-event%22%29)\n* [`otoroshi-env`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-env%22%29)\n* [`otoroshi-script-compiler`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-compiler%22%29)\n* [`otoroshi-script-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script-manager%22%29)\n* [`otoroshi-script`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-script%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-tcp-proxy`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-tcp-proxy%22%29)\n* [`otoroshi-custom-timeouts`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-custom-timeouts%22%29)\n* [`otoroshi-client-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-config%22%29)\n* [`otoroshi-canary`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-canary%22%29)\n* [`otoroshi-redirection-settings`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redirection-settings%22%29)\n* [`otoroshi-service-descriptor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor%22%29)\n* [`otoroshi-service-descriptor-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-descriptor-datastore%22%29)\n* [`otoroshi-console-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-console-mailer%22%29)\n* [`otoroshi-mailgun-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailgun-mailer%22%29)\n* [`otoroshi-mailjet-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-mailjet-mailer%22%29)\n* [`otoroshi-sendgrid-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sendgrid-mailer%22%29)\n* [`otoroshi-generic-mailer`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-generic-mailer%22%29)\n* [`otoroshi-clevercloud-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-clevercloud-client%22%29)\n* [`otoroshi-metrics`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-metrics%22%29)\n* [`otoroshi-gzip-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-gzip-config%22%29)\n* [`otoroshi-regex-pool`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-regex-pool%22%29)\n* [`otoroshi-ws-client-chooser`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ws-client-chooser%22%29)\n* [`otoroshi-akka-ws-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-akka-ws-client%22%29)\n* [`otoroshi-http-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-implicits%22%29)\n* [`otoroshi-service-group`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-group%22%29)\n* [`otoroshi-data-exporter-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config%22%29)\n* [`otoroshi-data-exporter-config-migration-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-data-exporter-config-migration-job%22%29)\n* [`otoroshi-lets-encrypt-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lets-encrypt-helper%22%29)\n* [`otoroshi-apkikey`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apkikey%22%29)\n* [`otoroshi-error-template`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-error-template%22%29)\n* [`otoroshi-job-manager`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-job-manager%22%29)\n* [`otoroshi-plugins-internal-eventlistener-actor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-internal-eventlistener-actor%22%29)\n* [`otoroshi-global-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-config%22%29)\n* [`otoroshi-jwks`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwks%22%29)\n* [`otoroshi-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-jwt-verifier%22%29)\n* [`otoroshi-global-jwt-verifier`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-jwt-verifier%22%29)\n* [`otoroshi-snowmonkey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-snowmonkey-config%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-webauthn-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-webauthn-admin-datastore%22%29)\n* [`otoroshi-service-datatstore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-service-datatstore%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-redis-like-store`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-like-store%22%29)\n* [`otoroshi-globalconfig-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-globalconfig-datastore%22%29)\n* [`otoroshi-reactive-pg-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-datastores%22%29)\n* [`otoroshi-reactive-pg-kv`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-reactive-pg-kv%22%29)\n* [`otoroshi-cassandra-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cassandra-datastores%22%29)\n* [`otoroshi-apikey-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikey-datastore%22%29)\n* [`otoroshi-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastore%22%29)\n* [`otoroshi-certificate-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificate-datastore%22%29)\n* [`otoroshi-simple-admin-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-simple-admin-datastore%22%29)\n* [`otoroshi-atomic-in-memory-datastore`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-atomic-in-memory-datastore%22%29)\n* [`otoroshi-lettuce-redis`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis%22%29)\n* [`otoroshi-lettuce-redis-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-lettuce-redis-cluster%22%29)\n* [`otoroshi-redis-lettuce-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-redis-lettuce-datastores%22%29)\n* [`otoroshi-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-datastores%22%29)\n* [`otoroshi-file-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-file-db-datastores%22%29)\n* [`otoroshi-http-db-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-http-db-datastores%22%29)\n* [`otoroshi-s3-datastores`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-s3-datastores%22%29)\n* [`PluginDocumentationGenerator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22PluginDocumentationGenerator%22%29)\n* [`otoroshi-health-checker`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-health-checker%22%29)\n* [`otoroshi-healthcheck-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-job%22%29)\n* [`otoroshi-healthcheck-local-cache-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-healthcheck-local-cache-job%22%29)\n* [`otoroshi-plugins-response-cache`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-response-cache%22%29)\n* [`otoroshi-oidc-apikey-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-oidc-apikey-config%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-info%22%29)\n* [`otoroshi-plugins-ipstack-geolocation-info`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-ipstack-geolocation-info%22%29)\n* [`otoroshi-plugins-maxmind-geolocation-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-maxmind-geolocation-helper%22%29)\n* [`otoroshi-plugins-user-agent-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-helper%22%29)\n* [`otoroshi-plugins-user-agent-extractor`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-user-agent-extractor%22%29)\n* [`otoroshi-global-el`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-el%22%29)\n* [`otoroshi-plugins-oauth1-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-oauth1-caller-plugin%22%29)\n* [`otoroshi-dynamic-sslcontext`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-dynamic-sslcontext%22%29)\n* [`otoroshi-plugins-access-log-clf`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-clf%22%29)\n* [`otoroshi-plugins-access-log-json`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-access-log-json%22%29)\n* [`otoroshi-plugins-kafka-access-log`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kafka-access-log%22%29)\n* [`otoroshi-plugins-kubernetes-client`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-client%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-ingress-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-ingress-sync%22%29)\n* [`otoroshi-plugins-kubernetes-crds-controller-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-controller-job%22%29)\n* [`otoroshi-plugins-kubernetes-crds-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-crds-sync%22%29)\n* [`otoroshi-cluster`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cluster%22%29)\n* [`otoroshi-crd-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-crd-validator%22%29)\n* [`otoroshi-sidecar-injector`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-sidecar-injector%22%29)\n* [`otoroshi-plugins-kubernetes-cert-sync`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-cert-sync%22%29)\n* [`otoroshi-plugins-kubernetes-to-otoroshi-certs-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-kubernetes-to-otoroshi-certs-job%22%29)\n* [`otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-otoroshi-certs-to-kubernetes-secrets-job%22%29)\n* [`otoroshi-apikeys-workflow-job`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-apikeys-workflow-job%22%29)\n* [`otoroshi-cert-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-helper%22%29)\n* [`otoroshi-certificates-ocsp`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-certificates-ocsp%22%29)\n* [`otoroshi-claim`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-claim%22%29)\n* [`otoroshi-cert`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert%22%29)\n* [`otoroshi-ssl-provider`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-provider%22%29)\n* [`otoroshi-cert-data`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-cert-data%22%29)\n* [`otoroshi-client-cert-validator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-client-cert-validator%22%29)\n* [`otoroshi-ssl-implicits`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ssl-implicits%22%29)\n* [`otoroshi-saml-validator-utils`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-saml-validator-utils%22%29)\n* [`otoroshi-global-saml-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-saml-config%22%29)\n* [`otoroshi-plugins-hmac-caller-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-caller-plugin%22%29)\n* [`otoroshi-plugins-hmac-access-validator-plugin`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hmac-access-validator-plugin%22%29)\n* [`otoroshi-plugins-hasallowedusersvalidator`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-hasallowedusersvalidator%22%29)\n* [`otoroshi-auth-module-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-auth-module-config%22%29)\n* [`otoroshi-basic-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-basic-auth-config%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n* [`otoroshi-plugins-jsonpath-helper`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-plugins-jsonpath-helper%22%29)\n* [`otoroshi-global-oauth2-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-config%22%29)\n* [`otoroshi-global-oauth2-module`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-global-oauth2-module%22%29)\n* [`otoroshi-ldap-auth-config`](https://github.com/MAIF/otoroshi/search?q=Logger%28%22otoroshi-ldap-auth-config%22%29)\n"},{"name":"end-to-end-mtls.md","id":"/how-to-s/end-to-end-mtls.md","url":"/how-to-s/end-to-end-mtls.html","title":"End-to-end mTLS","content":"# End-to-end mTLS\n\nIf you want to use MTLS on otoroshi, you first need to enable it. It is not enabled by default as it will make TLS handshake way heavier. \nTo enable it just change the following config :\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\nYou can use the `Want` setup if you cant to have both mtls on some services and no mtls on other services.\n\nYou can also change the trusted CA list sent in the handshake certificate request from the `Danger Zone` in `Tls Settings`.\n\nOtoroshi support mutual TLS out of the box. mTLS from client to Otoroshi and from Otoroshi to targets are supported. In this article we will see how to configure Otoroshi to use end-to-end mTLS. All code and files used in this articles can be found on the [Otoroshi github](https://github.com/MAIF/otoroshi/tree/master/demos/mtls)\n\n### Create certificates\n\nBut first we need to generate some certificates to make the demo work\n\n```sh\nmkdir mtls-demo\ncd mtls-demo\nmkdir ca\nmkdir server\nmkdir client\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-backend.key 4096\n# remove pass phrase\nopenssl rsa -in ./ca/ca-backend.key -out ./ca/ca-backend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-backend.key -out ./ca/ca-backend.cer -subj \"/CN=MTLSB\"\n\n\n# create a certificate authority key, use password as pass phrase\nopenssl genrsa -out ./ca/ca-frontend.key 2048\n# remove pass phrase\nopenssl rsa -in ./ca/ca-frontend.key -out ./ca/ca-frontend.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key ./ca/ca-frontend.key -out ./ca/ca-frontend.cer -subj \"/CN=MTLSF\"\n\n\n# now create the backend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.backend.oto.tools.key -out ./server/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.backend.oto.tools.key -sha256 -out ./server/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 1 -out ./server/_.backend.oto.tools.cer\n# verify the certificate, should output './server/_.backend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-backend.cer ./server/_.backend.oto.tools.cer\n\n\n# now create the frontend cert key, use password as pass phrase\nopenssl genrsa -out ./server/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./server/_.frontend.oto.tools.key -out ./server/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./server/_.frontend.oto.tools.key -sha256 -out ./server/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./server/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 1 -out ./server/_.frontend.oto.tools.cer\n# verify the certificate, should output './server/_.frontend.oto.tools.cer: OK'\nopenssl verify -CAfile ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.cer\n\n\n# now create the client cert key for backend, use password as pass phrase\nopenssl genrsa -out ./client/_.backend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.backend.oto.tools.key -out ./client/_.backend.oto.tools.csr -subj \"/CN=*.backend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.backend.oto.tools.csr -CA ./ca/ca-backend.cer -CAkey ./ca/ca-backend.key -set_serial 2 -out ./client/_.backend.oto.tools.cer\n# generate a pem version of the cert and key, use password as password\nopenssl x509 -in client/_.backend.oto.tools.cer -out client/_.backend.oto.tools.pem -outform PEM\n\n\n# now create the client cert key for frontend, use password as pass phrase\nopenssl genrsa -out ./client/_.frontend.oto.tools.key 2048\n# remove pass phrase\nopenssl rsa -in ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.key\n# generate the csr for the certificate\nopenssl req -new -key ./client/_.frontend.oto.tools.key -out ./client/_.frontend.oto.tools.csr -subj \"/CN=*.frontend.oto.tools\"\n# generate the certificate\nopenssl x509 -req -days 365 -sha256 -in ./client/_.frontend.oto.tools.csr -CA ./ca/ca-frontend.cer -CAkey ./ca/ca-frontend.key -set_serial 2 -out ./client/_.frontend.oto.tools.cer\n# generate a pkcs12 version of the cert and key, use password as password\n# openssl pkcs12 -export -clcerts -in client/_.frontend.oto.tools.cer -inkey client/_.frontend.oto.tools.key -out client/_.frontend.oto.tools.p12\nopenssl x509 -in client/_.frontend.oto.tools.cer -out client/_.frontend.oto.tools.pem -outform PEM\n```\n\nOnce it's done, you should have something like\n\n```sh\n$ tree\n.\n├── backend.js\n├── ca\n│   ├── ca-backend.cer\n│   ├── ca-backend.key\n│   ├── ca-frontend.cer\n│   └── ca-frontend.key\n├── client\n│   ├── _.backend.oto.tools.cer\n│   ├── _.backend.oto.tools.csr\n│   ├── _.backend.oto.tools.key\n│   ├── _.backend.oto.tools.pem\n│   ├── _.frontend.oto.tools.cer\n│   ├── _.frontend.oto.tools.csr\n│   ├── _.frontend.oto.tools.key\n│   └── _.frontend.oto.tools.pem\n└── server\n ├── _.backend.oto.tools.cer\n ├── _.backend.oto.tools.csr\n ├── _.backend.oto.tools.key\n ├── _.frontend.oto.tools.cer\n ├── _.frontend.oto.tools.csr\n └── _.frontend.oto.tools.key\n\n3 directories, 18 files\n```\n\n### The backend service \n\nnow, let's create a backend service using nodejs. Create a file named `backend.js`\n\n```sh\ntouch backend.js\n```\n\nand put the following content\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n}; \n\nconst server = https.createServer(options, (req, res) => { \n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nto run the server, just do \n\n```sh\nnode ./backend.js\n```\n\nnow you can try your server with\n\n```sh\ncurl --cacert ./ca/ca-backend.cer 'https://api.backend.oto.tools:8444/'\n```\n\nThis should output :\n```json\n{ \"message\": \"Hello World!\" }\n```\n\nnow modify your backend server to ensure that the client provides a client certificate like:\n\n```js\nconst fs = require('fs'); \nconst https = require('https'); \n\nconst options = { \n key: fs.readFileSync('./server/_.backend.oto.tools.key'), \n cert: fs.readFileSync('./server/_.backend.oto.tools.cer'), \n ca: fs.readFileSync('./ca/ca-backend.cer'), \n requestCert: true, \n rejectUnauthorized: true\n}; \n\nconst server = https.createServer(options, (req, res) => { \n console.log('Client certificate CN: ', req.socket.getPeerCertificate().subject.CN);\n res.writeHead(200, {\n 'Content-Type': 'application/json'\n }); \n res.end(JSON.stringify({ message: 'Hello World!' }) + \"\\n\"); \n}).listen(8444);\n\nconsole.log('Server listening:', `http://localhost:${server.address().port}`);\n```\n\nyou can test your new server with\n\n```sh\ncurl \\\n --cacert ./ca/ca-backend.cer \\\n --cert ./client/_.backend.oto.tools.pem \\\n --key ./client/_.backend.oto.tools.key 'https://api.backend.oto.tools:8444/'\n```\n\nthe output should be :\n\n```json\n{ \"message\": \"Hello World!\" }\n```\n\n### Otoroshi setup\n\nDownload the latest version of the Otoroshi jar and run it like\n\n```sh\n java \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.ssl.fromOutside.clientAuth=Want \\\n -jar -Dotoroshi.storage=file otoroshi.jar\n\n[info] otoroshi-env - Admin API exposed on http://otoroshi-api.oto.tools:8080\n[info] otoroshi-env - Admin UI exposed on http://otoroshi.oto.tools:8080\n[info] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[info] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[info] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / password\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n[info] p.c.s.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:8443\n[info] otoroshi-env - Generating a self signed SSL certificate for https://*.oto.tools ...\n```\n\nand log into otoroshi with the tuple `admin@otoroshi.io / password` displayed in the logs. \n\nOnce logged in, navigate to the routes page and create a new route.\n\n* Set a name then validate the creation\n* On frontend node, add `api.frontend.oto.tools` in the list of domains\n* On backend node, replace the target with `api.backend.oto.tools` as hostname and `8444` as port. \n\nSave the route and try to call it.\n\n```sh\ncurl 'http://api.frontend.oto.tools:8080/'\n```\n\nThis should output :\n```json\n{\"Otoroshi-Error\": \"Something went wrong, you should try later. Thanks for your understanding.\"}\n```\n\nyou should get an error due to the fact that Otoroshi doesn't know about the server certificate and the client certificate expected by the server.\n\nWe must declare the client and server certificates for `https://api.backend.oto.tools` to Otoroshi. \n\nGo to the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) and create a new item. Drag and drop the content of the `./client/_.backend.oto.tools.cer` and `./client/_.backend.oto.tools.key` files, respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you prefer to use the API, you can create an Otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.backend.oto.tools.cer ./ca/ca-backend.cer ./server/_.backend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n --data-binary @- \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle \n```\n\nnow we have to expose `https://api.frontend.oto.tools:8443` using otoroshi. \n\nCreate a second item. Copy and paste the content of `./server/_.frontend.oto.tools.cer` and `./server/_.frontend.oto.tools.key` respectively in `Certificate full chain` and `Certificate private key`.\n\nIf you don't want to bother with UI copy/paste, you can use the import bundle api endpoint to create an otoroshi certificate automatically from a PEM bundle.\n\n```sh\ncat ./server/_.frontend.oto.tools.cer ./ca/ca-frontend.cer ./server/_.frontend.oto.tools.key | curl \\\n -H 'Content-Type: text/plain' -X POST \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n --data-binary @- \\\n http://otoroshi-api.oto.tools:8080/api/certificates/_bundle\n```\n\nOnce created, go back to your route. On the target of the backend node, we have to enable the custom Otoroshi TLS.\n\n* Click on the backend node\n* Click on your target\n* Click on the Show advanced settings button\n* Click on Custom TLS setup\n* Enable the section\n* In the list of certificates, select the backend certificate\n* In the list of trusted certificates, select the frontend certificate\n* Save your route\n \nTry the following command\n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\nNow we want to enforce the fact that we want client certificate for `api.frontend.oto.tools`. \n\nSearch in the list of plugins and add the `Client Certificate Only` plugin to your route.\n\nnow if you retry \n\n```sh\ncurl --cacert ./ca/ca-frontend.cer 'https://api.frontend.oto.tools:8443/'\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nyou should get an error because no client certificate is passed with the request. But if you pass the `./client/_.frontend.oto.tools.csr` client cert and the key in your curl call\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n### Client certificate matching plugin\n\nOtoroshi can restrict and check all incoming client certificates on a route.\n\nSearch in the list of plugins the `Client certificate matching` plugin and add it the the flow.\n\nSave the route and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"Otoroshi-Error\":\"bad request\"}\n```\n\nOur client certificate is not matched by Otoroshi. We have to add the subject DN in the configuration of the `Client certificate matching` plugin to authorize it.\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [],\n \"subjectDNs\": [\n \"CN=*.frontend.oto.tools\"\n ],\n \"issuerDNs\": [],\n \"regexSubjectDNs\": [],\n \"regexIssuerDNs\": []\n }\n}\n```\n\nSave the service and retry your call again.\n\n```sh\ncurl 'https://api.frontend.oto.tools:8443' \\\n --cacert ./ca/ca-frontend.cer \\\n --cert ./client/_.frontend.oto.tools.pem \\\n --key ./client/_.frontend.oto.tools.key\n```\nthe output should be\n\n```json\n{\"message\":\"Hello World!\"}\n```\n\n\n"},{"name":"export-alerts-using-mailgun.md","id":"/how-to-s/export-alerts-using-mailgun.md","url":"/how-to-s/export-alerts-using-mailgun.html","title":"Send alerts using mailgun","content":"# Send alerts using mailgun\n\nAll Otoroshi alerts can be send on different channels.\nOne of the ways is to send a group of specific alerts via emails.\n\nTo enable this behaviour, let's start by create an exporter of events.\n\nIn this tutorial, we will admit that you already have a mailgun account with an API key and a domain.\n\n## Create an Mailgun exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `mailer` in the `type` selector field\n6. Jump to `Exporter config` and select the `Mailgun` option\n7. Set the following values:\n* `EU` : false/true depending on your mailgun configuratin\n* `Mailgun api key` : your-mailgun-api-key\n* `Mailgun domain` : your-mailgun-domain\n* `Email addresses` : list of the recipient adresses\n\nWith this configuration, all Otoroshi events will be send to your listed addresses (we don't recommended to do that).\n\nTo filter events on `Alerts` type, we need to add the following configuration inside the `Filtering and projection` section (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)).\n\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n``` \n\nSave at the bottom page and enable the exporter (on the top of the page or in list of exporters). We will need to wait few seconds to receive the first alerts.\n\nThe **projection** field can be useful in the case you want to filter the fields contained in each alert sent.\n\nThe `Projection` field is a json where you can list the fields to keep for each alert.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be sent to the addresses of your recipients."},{"name":"export-events-to-elastic.md","id":"/how-to-s/export-events-to-elastic.md","url":"/how-to-s/export-events-to-elastic.html","title":"Export events to Elasticsearch","content":"# Export events to Elasticsearch\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Deploy a Elasticsearch and kibana stack on Docker\n\nLet's start by creating an Elasticsearch and Kibana stack on our machine (if it's already done for you, you can skip this section).\n\nTo start an Elasticsearch container for development or testing, run:\n\n```sh\ndocker network create elastic\ndocker pull docker.elastic.co/elasticsearch/elasticsearch:7.15.1\ndocker run --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e \"discovery.type=single-node\" docker.elastic.co/elasticsearch/elasticsearch:7.15.1\n```\n\n```sh\ndocker pull docker.elastic.co/kibana/kibana:7.15.1\ndocker run --name kib01-test --net elastic -p 5601:5601 -e \"ELASTICSEARCH_HOSTS=http://es01-test:9200\" docker.elastic.co/kibana/kibana:7.15.1\n```\n\nTo access Kibana, go to @link:[http://localhost:5601](http://localhost:5601) { open=new }.\n\n### Create an Elasticsearch exporter\n\nLet's create an exporter. The exporter will export by default all events generated by Otoroshi.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n2. Click on the cog icon on the top right\n3. Then `Exporters` button\n4. And add a new configuration when clicking on the `Add item` button\n5. Select the `elastic` in the `type` selector field\n6. Jump to `Exporter config`\n7. Set the following values: `Cluster URI` -> `http://localhost:9200`\n\nThen test your configuration by clicking on the `Check connection` button. This should output a modal with the Elasticsearch version and the number of loaded docs.\n\nSave at the bottom of the page and enable the exporter (on the top of the page or in list of exporters).\n\n### Testing your configuration\n\nOne simple way to test is to setup the reading of our Elasticsearch instance by Otoroshi.\n\nNavigate to the danger zone (click on the cog on the top right and scroll to `danger zone`). Jump to the `Analytics: Elastic dashboard datasource (read)` section.\n\nSet the following values : `Cluster URI` -> `http://localhost:9200`\n\nThen click on the `Check connection`. This should ouput the same result as the previous part. Save the global configuration and navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/stats](http://otoroshi.oto.tools:8080/bo/dashboard/stats) { open=new }.\n\nThis should output a list of graphs.\n\n### Advanced usage\n\nBy default, an exporter handle all events from Otoroshi. In some case, you need to filter the events to send to elasticsearch.\n\nTo filter the events, jump to the `Filtering and projection` field in exporter view. Otoroshi supports to include a kind of events or to exclude a list of events (if you want to deep learn about this section, read this @ref:[part](../entities/data-exporters.md#matching-and-projections)). \n\nAn example which keep only events with a field `@type` of value `AlertEvent`:\n```json\n{\n \"include\": [\n { \"@type\": \"AlertEvent\" }\n ],\n \"exclude\": []\n}\n```\nAn example which exclude only events with a field `@type` of value `GatewayEvent` :\n```json\n{\n \"exclude\": [\n { \"@type\": \"GatewayEvent\" }\n ],\n \"include\": []\n}\n```\n\nThe next field is the **Projection**. This field is a json when you can list the fields to keep for each event.\n\n```json\n{\n \"@type\": true,\n \"@timestamp\": true,\n \"@id\": true\n}\n```\n\nWith this example, only `@type`, `@timestamp` and `@id` will be send to ES.\n\n### Debug your configuration\n\n#### Missing user rights on Elasticsearch\n\nWhen creating an exporter, Otoroshi try to join the index route of the elasticsearch instance. If you have a specific management access rights on Elasticsearch, you have two possiblities :\n\n- set a full access to the user used in Otoroshi for write in Elasticsearch\n- set the version of Elasticsearch inside the `Version` field of your exporter.\n\n#### None event appear in your Elasticsearch\n\nWhen creating an exporter, Otoroshi try to push the index template on Elasticsearch. If the post failed, Otoroshi will fail for each push of events and your database will keep empty. \n\nTo fix this problem, you can try to send the index template with the `Manually apply index template` button in your exporter."},{"name":"import-export-otoroshi-datastore.md","id":"/how-to-s/import-export-otoroshi-datastore.md","url":"/how-to-s/import-export-otoroshi-datastore.html","title":"Import and export Otoroshi datastore","content":"# Import and export Otoroshi datastore\n\n### Start Otoroshi with an initial datastore\n\nLet's start by downloading the latest Otoroshi\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nBy default, Otoroshi starts with domain `oto.tools` that targets `127.0.0.1` Now you are almost ready to run Otoroshi for the first time, we want run it with an initial data.\n\nTo do that, you need to add the **otoroshi.importFrom** setting to the Otoroshi configuration (of `$APP_IMPORT_FROM` env). It can be a file path or a URL. The content of the initial datastore can look something like the following.\n\n```json\n{\n \"label\": \"Otoroshi initial datastore\",\n \"admins\": [],\n \"simpleAdmins\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"username\": \"admin@otoroshi.io\",\n \"password\": \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\": \"Otoroshi Admin\",\n \"createdAt\": 1634651307724,\n \"type\": \"SIMPLE\",\n \"metadata\": {},\n \"tags\": [],\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n }\n ],\n \"serviceGroups\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-group\",\n \"name\": \"Otoroshi Admin Api group\",\n \"description\": \"No description\",\n \"tags\": [],\n \"metadata\": {}\n },\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"default\",\n \"name\": \"default-group\",\n \"description\": \"The default service group\",\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"apiKeys\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"clientId\": \"admin-api-apikey-id\",\n \"clientSecret\": \"admin-api-apikey-secret\",\n \"clientName\": \"Otoroshi Backoffice ApiKey\",\n \"description\": \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\": \"admin-api-group\",\n \"authorizedEntities\": [\n \"group_admin-api-group\"\n ],\n \"enabled\": true,\n \"readOnly\": false,\n \"allowClientIdOnly\": false,\n \"throttlingQuota\": 10000,\n \"dailyQuota\": 10000000,\n \"monthlyQuota\": 10000000,\n \"constrainedServicesOnly\": false,\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"rotation\": {\n \"enabled\": false,\n \"rotationEvery\": 744,\n \"gracePeriod\": 168,\n \"nextSecret\": null\n },\n \"validUntil\": null,\n \"tags\": [],\n \"metadata\": {}\n }\n ],\n \"serviceDescriptors\": [\n {\n \"_loc\": {\n \"tenant\": \"default\",\n \"teams\": [\n \"default\"\n ]\n },\n \"id\": \"admin-api-service\",\n \"groupId\": \"admin-api-group\",\n \"groups\": [\n \"admin-api-group\"\n ],\n \"name\": \"otoroshi-admin-api\",\n \"description\": \"\",\n \"env\": \"prod\",\n \"domain\": \"oto.tools\",\n \"subdomain\": \"otoroshi-api\",\n \"targetsLoadBalancing\": {\n \"type\": \"RoundRobin\"\n },\n \"targets\": [\n {\n \"host\": \"127.0.0.1:8080\",\n \"scheme\": \"http\",\n \"weight\": 1,\n \"mtlsConfig\": {\n \"certs\": [],\n \"trustedCerts\": [],\n \"mtls\": false,\n \"loose\": false,\n \"trustAll\": false\n },\n \"tags\": [],\n \"metadata\": {},\n \"protocol\": \"HTTP/1.1\",\n \"predicate\": {\n \"type\": \"AlwaysMatch\"\n },\n \"ipAddress\": null\n }\n ],\n \"root\": \"/\",\n \"matchingRoot\": null,\n \"stripPath\": true,\n \"localHost\": \"127.0.0.1:8080\",\n \"localScheme\": \"http\",\n \"redirectToLocal\": false,\n \"enabled\": true,\n \"userFacing\": false,\n \"privateApp\": false,\n \"forceHttps\": false,\n \"logAnalyticsOnServer\": false,\n \"useAkkaHttpClient\": true,\n \"useNewWSClient\": false,\n \"tcpUdpTunneling\": false,\n \"detectApiKeySooner\": false,\n \"maintenanceMode\": false,\n \"buildMode\": false,\n \"strictlyPrivate\": false,\n \"enforceSecureCommunication\": true,\n \"sendInfoToken\": true,\n \"sendStateChallenge\": true,\n \"sendOtoroshiHeadersBack\": true,\n \"readOnly\": false,\n \"xForwardedHeaders\": false,\n \"overrideHost\": true,\n \"allowHttp10\": true,\n \"letsEncrypt\": false,\n \"secComHeaders\": {\n \"claimRequestName\": null,\n \"stateRequestName\": null,\n \"stateResponseName\": null\n },\n \"secComTtl\": 30000,\n \"secComVersion\": 1,\n \"secComInfoTokenVersion\": \"Legacy\",\n \"secComExcludedPatterns\": [],\n \"securityExcludedPatterns\": [],\n \"publicPatterns\": [\n \"/health\",\n \"/metrics\"\n ],\n \"privatePatterns\": [],\n \"additionalHeaders\": {\n \"Host\": \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\": {},\n \"missingOnlyHeadersIn\": {},\n \"missingOnlyHeadersOut\": {},\n \"removeHeadersIn\": [],\n \"removeHeadersOut\": [],\n \"headersVerification\": {},\n \"matchingHeaders\": {},\n \"ipFiltering\": {\n \"whitelist\": [],\n \"blacklist\": []\n },\n \"api\": {\n \"exposeApi\": false\n },\n \"healthCheck\": {\n \"enabled\": false,\n \"url\": \"/\"\n },\n \"clientConfig\": {\n \"useCircuitBreaker\": true,\n \"retries\": 1,\n \"maxErrors\": 20,\n \"retryInitialDelay\": 50,\n \"backoffFactor\": 2,\n \"callTimeout\": 30000,\n \"callAndStreamTimeout\": 120000,\n \"connectionTimeout\": 10000,\n \"idleTimeout\": 60000,\n \"globalTimeout\": 30000,\n \"sampleInterval\": 2000,\n \"proxy\": {},\n \"customTimeouts\": [],\n \"cacheConnectionSettings\": {\n \"enabled\": false,\n \"queueSize\": 2048\n }\n },\n \"canary\": {\n \"enabled\": false,\n \"traffic\": 0.2,\n \"targets\": [],\n \"root\": \"/\"\n },\n \"gzip\": {\n \"enabled\": false,\n \"excludedPatterns\": [],\n \"whiteList\": [\n \"text/*\",\n \"application/javascript\",\n \"application/json\"\n ],\n \"blackList\": [],\n \"bufferSize\": 8192,\n \"chunkedThreshold\": 102400,\n \"compressionLevel\": 5\n },\n \"metadata\": {},\n \"tags\": [],\n \"chaosConfig\": {\n \"enabled\": false,\n \"largeRequestFaultConfig\": null,\n \"largeResponseFaultConfig\": null,\n \"latencyInjectionFaultConfig\": null,\n \"badResponsesFaultConfig\": null\n },\n \"jwtVerifier\": {\n \"type\": \"ref\",\n \"ids\": [],\n \"id\": null,\n \"enabled\": false,\n \"excludedPatterns\": []\n },\n \"secComSettings\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComUseSameAlgo\": true,\n \"secComAlgoChallengeOtoToBack\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoChallengeBackToOto\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"secComAlgoInfoToken\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"cors\": {\n \"enabled\": false,\n \"allowOrigin\": \"*\",\n \"exposeHeaders\": [],\n \"allowHeaders\": [],\n \"allowMethods\": [],\n \"excludedPatterns\": [],\n \"maxAge\": null,\n \"allowCredentials\": true\n },\n \"redirection\": {\n \"enabled\": false,\n \"code\": 303,\n \"to\": \"https://www.otoroshi.io\"\n },\n \"authConfigRef\": null,\n \"clientValidatorRef\": null,\n \"transformerRef\": null,\n \"transformerRefs\": [],\n \"transformerConfig\": {},\n \"apiKeyConstraints\": {\n \"basicAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"customHeadersAuth\": {\n \"enabled\": true,\n \"clientIdHeaderName\": null,\n \"clientSecretHeaderName\": null\n },\n \"clientIdAuth\": {\n \"enabled\": true,\n \"headerName\": null,\n \"queryName\": null\n },\n \"jwtAuth\": {\n \"enabled\": true,\n \"secretSigned\": true,\n \"keyPairSigned\": true,\n \"includeRequestAttributes\": false,\n \"maxJwtLifespanSecs\": null,\n \"headerName\": null,\n \"queryName\": null,\n \"cookieName\": null\n },\n \"routing\": {\n \"noneTagIn\": [],\n \"oneTagIn\": [],\n \"allTagsIn\": [],\n \"noneMetaIn\": {},\n \"oneMetaIn\": {},\n \"allMetaIn\": {},\n \"noneMetaKeysIn\": [],\n \"oneMetaKeyIn\": [],\n \"allMetaKeysIn\": []\n }\n },\n \"restrictions\": {\n \"enabled\": false,\n \"allowLast\": true,\n \"allowed\": [],\n \"forbidden\": [],\n \"notFound\": []\n },\n \"accessValidator\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"preRouting\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excludedPatterns\": []\n },\n \"plugins\": {\n \"enabled\": false,\n \"refs\": [],\n \"config\": {},\n \"excluded\": []\n },\n \"hosts\": [\n \"otoroshi-api.oto.tools\"\n ],\n \"paths\": [],\n \"handleLegacyDomain\": true,\n \"issueCert\": false,\n \"issueCertCA\": null\n }\n ],\n \"errorTemplates\": [],\n \"jwtVerifiers\": [],\n \"authConfigs\": [],\n \"certificates\": [],\n \"clientValidators\": [],\n \"scripts\": [],\n \"tcpServices\": [],\n \"dataExporters\": [],\n \"tenants\": [\n {\n \"id\": \"default\",\n \"name\": \"Default organization\",\n \"description\": \"The default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ],\n \"teams\": [\n {\n \"id\": \"default\",\n \"tenant\": \"default\",\n \"name\": \"Default Team\",\n \"description\": \"The default Team of the default organization\",\n \"metadata\": {},\n \"tags\": []\n }\n ]\n}\n```\n\nRun an Otoroshi with the previous file as parameter.\n\n```sh\njava \\\n -Dotoroshi.adminPassword=password \\\n -Dotoroshi.importFrom=./initial-state.json \\\n -jar otoroshi.jar \n```\n\nThis should show\n\n```sh\n...\n[info] otoroshi-env - Importing from: ./initial-state.json\n[info] otoroshi-env - Successful import !\n...\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n...\n```\n\n> Warning : when you using Otoroshi with a datastore different from file or in-memory, Otoroshi will not reload the initialization script. If you expected, you have to manually clean your store.\n\n### Export the current datastore via the danger zone\n\nWhen Otoroshi is running, you can backup the global configuration store from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom page. \n\nClick on `Full export` button to download the full global configuration.\n\n### Import a datastore from file via the danger zone\n\nWhen Otoroshi is running, you can recover a global configuration from the UI. Navigate to your instance (in our case @link:[http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone) { open=new }) and scroll to the bottom of the page. \n\nClick on `Recover from a full export file` button to apply all configurations from a file.\n\n### Export the current datastore with the Admin API\n\nOtoroshi exposes his own Admin API to manage Otoroshi resources. To call this api, you need to have an api key with the rights on `Otoroshi Admin Api group`. This group includes the `Otoroshi-admin-api` service that you can found on the services page. \n\nBy default, and with our initial configuration, Otoroshi has already created an api key named `Otoroshi Backoffice ApiKey`. You can verify the rights of an api key on its page by checking the `Authorized On` field (you should find the `Otoroshi Admin Api group` inside).\n\nThe default api key id and secret are `admin-api-apikey-id` and `admin-api-apikey-secret`.\n\nRun the next command with these values.\n\n```sh\ncurl \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json'\n```\n\nWhen calling the `/api/otoroshi.json`, the return should be the current datastore including the service descriptors, the api keys, all others resources like certificates and authentification modules, and the the global config (representing the form of the danger zone).\n\n### Import the current datastore with the Admin API\n\nAs the same way of previous section, you can erase the current datastore with a POST request. The route is the same : `/api/otoroshi.json`.\n\n```sh\ncurl \\\n -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\" : \"Otoroshi export\",\n \"dateRaw\" : 1634714811217,\n \"date\" : \"2021-10-20 09:26:51\",\n \"stats\" : {\n \"calls\" : 4,\n \"dataIn\" : 0,\n \"dataOut\" : 97991\n },\n \"config\" : {\n \"tags\" : [ ],\n \"letsEncryptSettings\" : {\n \"enabled\" : false,\n \"server\" : \"acme://letsencrypt.org/staging\",\n \"emails\" : [ ],\n \"contacts\" : [ ],\n \"publicKey\" : \"\",\n \"privateKey\" : \"\"\n },\n \"lines\" : [ \"prod\" ],\n \"maintenanceMode\" : false,\n \"enableEmbeddedMetrics\" : true,\n \"streamEntityOnly\" : true,\n \"autoLinkToDefaultGroup\" : true,\n \"limitConcurrentRequests\" : false,\n \"maxConcurrentRequests\" : 1000,\n \"maxHttp10ResponseSize\" : 4194304,\n \"useCircuitBreakers\" : true,\n \"apiReadOnly\" : false,\n \"u2fLoginOnly\" : false,\n \"trustXForwarded\" : true,\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"throttlingQuota\" : 10000000,\n \"perIpThrottlingQuota\" : 10000000,\n \"analyticsWebhooks\" : [ ],\n \"alertsWebhooks\" : [ ],\n \"elasticWritesConfigs\" : [ ],\n \"elasticReadsConfig\" : null,\n \"alertsEmails\" : [ ],\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : false,\n \"endlessIpAddresses\" : [ ],\n \"statsdConfig\" : null,\n \"kafkaConfig\" : {\n \"servers\" : [ ],\n \"keyPass\" : null,\n \"keystore\" : null,\n \"truststore\" : null,\n \"topic\" : \"otoroshi-events\",\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n },\n \"backOfficeAuthRef\" : null,\n \"mailerSettings\" : {\n \"type\" : \"none\"\n },\n \"cleverSettings\" : null,\n \"maxWebhookSize\" : 100,\n \"middleFingers\" : false,\n \"maxLogsSize\" : 10000,\n \"otoroshiId\" : \"83539cbca-76ee-4abc-ad31-a4794e873848\",\n \"snowMonkeyConfig\" : {\n \"enabled\" : false,\n \"outageStrategy\" : \"OneServicePerGroup\",\n \"includeUserFacingDescriptors\" : false,\n \"dryRun\" : false,\n \"timesPerDay\" : 1,\n \"startTime\" : \"09:00:00.000\",\n \"stopTime\" : \"23:59:59.000\",\n \"outageDurationFrom\" : 600000,\n \"outageDurationTo\" : 3600000,\n \"targetGroups\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : true,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : {\n \"ratio\" : 0.2,\n \"from\" : 500,\n \"to\" : 5000\n },\n \"badResponsesFaultConfig\" : {\n \"ratio\" : 0.2,\n \"responses\" : [ {\n \"status\" : 502,\n \"body\" : \"{\\\"error\\\":\\\"Nihonzaru everywhere ...\\\"}\",\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n }\n } ]\n }\n }\n },\n \"scripts\" : {\n \"enabled\" : false,\n \"transformersRefs\" : [ ],\n \"transformersConfig\" : { },\n \"validatorRefs\" : [ ],\n \"validatorConfig\" : { },\n \"preRouteRefs\" : [ ],\n \"preRouteConfig\" : { },\n \"sinkRefs\" : [ ],\n \"sinkConfig\" : { },\n \"jobRefs\" : [ ],\n \"jobConfig\" : { }\n },\n \"geolocationSettings\" : {\n \"type\" : \"none\"\n },\n \"userAgentSettings\" : {\n \"enabled\" : false\n },\n \"autoCert\" : {\n \"enabled\" : false,\n \"replyNicely\" : false,\n \"caRef\" : null,\n \"allowed\" : [ ],\n \"notAllowed\" : [ ]\n },\n \"tlsSettings\" : {\n \"defaultDomain\" : null,\n \"randomIfNotFound\" : false,\n \"includeJdkCaServer\" : true,\n \"includeJdkCaClient\" : true,\n \"trustedCAsServer\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"metadata\" : { }\n },\n \"admins\" : [ ],\n \"simpleAdmins\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"username\" : \"admin@otoroshi.io\",\n \"password\" : \"$2a$10$iQRkqjKTW.5XH8ugQrnMDeUstx4KqmIeQ58dHHdW2Dv1FkyyAs4C.\",\n \"label\" : \"Otoroshi Admin\",\n \"createdAt\" : 1634651307724,\n \"type\" : \"SIMPLE\",\n \"metadata\" : { },\n \"tags\" : [ ],\n \"rights\" : [ {\n \"tenant\" : \"*:rw\",\n \"teams\" : [ \"*:rw\" ]\n } ]\n } ],\n \"serviceGroups\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-group\",\n \"name\" : \"Otoroshi Admin Api group\",\n \"description\" : \"No description\",\n \"tags\" : [ ],\n \"metadata\" : { }\n }, {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"default\",\n \"name\" : \"default-group\",\n \"description\" : \"The default service group\",\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"apiKeys\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"clientId\" : \"admin-api-apikey-id\",\n \"clientSecret\" : \"admin-api-apikey-secret\",\n \"clientName\" : \"Otoroshi Backoffice ApiKey\",\n \"description\" : \"The apikey use by the Otoroshi UI\",\n \"authorizedGroup\" : \"admin-api-group\",\n \"authorizedEntities\" : [ \"group_admin-api-group\" ],\n \"enabled\" : true,\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 10000,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"rotation\" : {\n \"enabled\" : false,\n \"rotationEvery\" : 744,\n \"gracePeriod\" : 168,\n \"nextSecret\" : null\n },\n \"validUntil\" : null,\n \"tags\" : [ ],\n \"metadata\" : { }\n } ],\n \"serviceDescriptors\" : [ {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"admin-api-service\",\n \"groupId\" : \"admin-api-group\",\n \"groups\" : [ \"admin-api-group\" ],\n \"name\" : \"otoroshi-admin-api\",\n \"description\" : \"\",\n \"env\" : \"prod\",\n \"domain\" : \"oto.tools\",\n \"subdomain\" : \"otoroshi-api\",\n \"targetsLoadBalancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"targets\" : [ {\n \"host\" : \"127.0.0.1:8080\",\n \"scheme\" : \"http\",\n \"weight\" : 1,\n \"mtlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n },\n \"tags\" : [ ],\n \"metadata\" : { },\n \"protocol\" : \"HTTP/1.1\",\n \"predicate\" : {\n \"type\" : \"AlwaysMatch\"\n },\n \"ipAddress\" : null\n } ],\n \"root\" : \"/\",\n \"matchingRoot\" : null,\n \"stripPath\" : true,\n \"localHost\" : \"127.0.0.1:8080\",\n \"localScheme\" : \"http\",\n \"redirectToLocal\" : false,\n \"enabled\" : true,\n \"userFacing\" : false,\n \"privateApp\" : false,\n \"forceHttps\" : false,\n \"logAnalyticsOnServer\" : false,\n \"useAkkaHttpClient\" : true,\n \"useNewWSClient\" : false,\n \"tcpUdpTunneling\" : false,\n \"detectApiKeySooner\" : false,\n \"maintenanceMode\" : false,\n \"buildMode\" : false,\n \"strictlyPrivate\" : false,\n \"enforceSecureCommunication\" : true,\n \"sendInfoToken\" : true,\n \"sendStateChallenge\" : true,\n \"sendOtoroshiHeadersBack\" : true,\n \"readOnly\" : false,\n \"xForwardedHeaders\" : false,\n \"overrideHost\" : true,\n \"allowHttp10\" : true,\n \"letsEncrypt\" : false,\n \"secComHeaders\" : {\n \"claimRequestName\" : null,\n \"stateRequestName\" : null,\n \"stateResponseName\" : null\n },\n \"secComTtl\" : 30000,\n \"secComVersion\" : 1,\n \"secComInfoTokenVersion\" : \"Legacy\",\n \"secComExcludedPatterns\" : [ ],\n \"securityExcludedPatterns\" : [ ],\n \"publicPatterns\" : [ \"/health\", \"/metrics\" ],\n \"privatePatterns\" : [ ],\n \"additionalHeaders\" : {\n \"Host\" : \"otoroshi-admin-internal-api.oto.tools\"\n },\n \"additionalHeadersOut\" : { },\n \"missingOnlyHeadersIn\" : { },\n \"missingOnlyHeadersOut\" : { },\n \"removeHeadersIn\" : [ ],\n \"removeHeadersOut\" : [ ],\n \"headersVerification\" : { },\n \"matchingHeaders\" : { },\n \"ipFiltering\" : {\n \"whitelist\" : [ ],\n \"blacklist\" : [ ]\n },\n \"api\" : {\n \"exposeApi\" : false\n },\n \"healthCheck\" : {\n \"enabled\" : false,\n \"url\" : \"/\"\n },\n \"clientConfig\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n },\n \"canary\" : {\n \"enabled\" : false,\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n },\n \"gzip\" : {\n \"enabled\" : false,\n \"excludedPatterns\" : [ ],\n \"whiteList\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blackList\" : [ ],\n \"bufferSize\" : 8192,\n \"chunkedThreshold\" : 102400,\n \"compressionLevel\" : 5\n },\n \"metadata\" : { },\n \"tags\" : [ ],\n \"chaosConfig\" : {\n \"enabled\" : false,\n \"largeRequestFaultConfig\" : null,\n \"largeResponseFaultConfig\" : null,\n \"latencyInjectionFaultConfig\" : null,\n \"badResponsesFaultConfig\" : null\n },\n \"jwtVerifier\" : {\n \"type\" : \"ref\",\n \"ids\" : [ ],\n \"id\" : null,\n \"enabled\" : false,\n \"excludedPatterns\" : [ ]\n },\n \"secComSettings\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComUseSameAlgo\" : true,\n \"secComAlgoChallengeOtoToBack\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoChallengeBackToOto\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"secComAlgoInfoToken\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"cors\" : {\n \"enabled\" : false,\n \"allowOrigin\" : \"*\",\n \"exposeHeaders\" : [ ],\n \"allowHeaders\" : [ ],\n \"allowMethods\" : [ ],\n \"excludedPatterns\" : [ ],\n \"maxAge\" : null,\n \"allowCredentials\" : true\n },\n \"redirection\" : {\n \"enabled\" : false,\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n },\n \"authConfigRef\" : null,\n \"clientValidatorRef\" : null,\n \"transformerRef\" : null,\n \"transformerRefs\" : [ ],\n \"transformerConfig\" : { },\n \"apiKeyConstraints\" : {\n \"basicAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"customHeadersAuth\" : {\n \"enabled\" : true,\n \"clientIdHeaderName\" : null,\n \"clientSecretHeaderName\" : null\n },\n \"clientIdAuth\" : {\n \"enabled\" : true,\n \"headerName\" : null,\n \"queryName\" : null\n },\n \"jwtAuth\" : {\n \"enabled\" : true,\n \"secretSigned\" : true,\n \"keyPairSigned\" : true,\n \"includeRequestAttributes\" : false,\n \"maxJwtLifespanSecs\" : null,\n \"headerName\" : null,\n \"queryName\" : null,\n \"cookieName\" : null\n },\n \"routing\" : {\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : { },\n \"oneMetaIn\" : { },\n \"allMetaIn\" : { },\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n },\n \"restrictions\" : {\n \"enabled\" : false,\n \"allowLast\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"notFound\" : [ ]\n },\n \"accessValidator\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"preRouting\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excludedPatterns\" : [ ]\n },\n \"plugins\" : {\n \"enabled\" : false,\n \"refs\" : [ ],\n \"config\" : { },\n \"excluded\" : [ ]\n },\n \"hosts\" : [ \"otoroshi-api.oto.tools\" ],\n \"paths\" : [ ],\n \"handleLegacyDomain\" : true,\n \"issueCert\" : false,\n \"issueCertCA\" : null\n } ],\n \"errorTemplates\" : [ ],\n \"jwtVerifiers\" : [ ],\n \"authConfigs\" : [ ],\n \"certificates\" : [],\n \"clientValidators\" : [ ],\n \"scripts\" : [ ],\n \"tcpServices\" : [ ],\n \"dataExporters\" : [ ],\n \"tenants\" : [ {\n \"id\" : \"default\",\n \"name\" : \"Default organization\",\n \"description\" : \"The default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ],\n \"teams\" : [ {\n \"id\" : \"default\",\n \"tenant\" : \"default\",\n \"name\" : \"Default Team\",\n \"description\" : \"The default Team of the default organization\",\n \"metadata\" : { },\n \"tags\" : [ ]\n } ]\n }' \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note : be very carefully with this POST command. If you send a wrong JSON, you risk breaking your instance.\n\nThe second way is to send the same configuration but from a file. You can pass two kind of file : a `json` file or a `ndjson` file. Both files are available as export methods on the danger zone.\n\n```sh\n# the curl is run from a folder containing the initial-state.json file \ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d @./initial-state.json \\\n 'http://otoroshi-api.oto.tools:8080/api/otoroshi.json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThis should output :\n\n```json\n{ \"done\":true }\n```\n\n> Note: To send a ndjson file, you have to set the Content-Type header at `application/x-ndjson`"},{"name":"index.md","id":"/how-to-s/index.md","url":"/how-to-s/index.html","title":"How to's","content":"# How to's\n\nin this section, we will explain some mainstream Otoroshi usage scenario's \n\n* @ref:[Otoroshi and WASM](./wasm-usage.md)\n* @ref:[Wasmo](./wasmo-installation.md)\n* @ref:[Tailscale integration](./tailscale-integration.md)\n* @ref:[End-to-end mTLS](./end-to-end-mtls.md)\n* @ref:[Send alerts by emails](./export-alerts-using-mailgun.md)\n* @ref:[Export events to Elasticsearch](./export-events-to-elastic.md)\n* @ref:[Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* @ref:[Secure an app with Auth0](./secure-app-with-auth0.md)\n* @ref:[Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* @ref:[Secure an app with LDAP](./secure-app-with-ldap.md)\n* @ref:[Secure an api with apikeys](./secure-with-apikey.md)\n* @ref:[Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* @ref:[Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* @ref:[Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* @ref:[TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* @ref:[Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* @ref:[Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* @ref:[TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* @ref:[The resources loader](./resources-loader.md)\n* @ref:[Log levels customization](./custom-log-levels.md)\n* @ref:[Initial state customization](./custom-initial-state.md)\n* @ref:[Communicate with Kafka](./communicate-with-kafka.md)\n* @ref:[Create your custom Authentication module](./create-custom-auth-module.md)\n* @ref:[Working with Eureka](./working-with-eureka.md)\n* @ref:[Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* @ref:[Quickly expose a website and static files](./zip-backend-plugin.md)\n\n@@@ index\n\n\n* [WASM usage](./wasm-usage.md)\n* [wasmo](./wasmo-installation.md)\n* [Tailscale integration](./tailscale-integration.md)\n* [End-to-end mTLS](./end-to-end-mtls.md)\n* [Send alerts by emails](./export-alerts-using-mailgun.md)\n* [Export events to Elasticsearch](./export-events-to-elastic.md)\n* [Import/export Otoroshi datastore](./import-export-otoroshi-datastore.md)\n* [Secure an app with Auth0](./secure-app-with-auth0.md)\n* [Secure an app with Keycloak](./secure-app-with-keycloak.md)\n* [Secure an app with LDAP](./secure-app-with-ldap.md)\n* [Secure an api with apikeys](./secure-with-apikey.md)\n* [Secure an app with OAuth1](./secure-with-oauth1-client.md)\n* [Secure an api with OAuth2 client_credentials flow](./secure-with-oauth2-client-credentials.md)\n* [Setup an Otoroshi cluster](./setup-otoroshi-cluster.md)\n* [TLS termination using Let's Encrypt](./tls-using-lets-encrypt.md)\n* [Secure an app with jwt verifiers](./secure-an-app-with-jwt-verifiers.md)\n* [Secure the communication between a backend app and Otoroshi](./secure-the-communication-between-a-backend-app-and-otoroshi.md)\n* [TLS termination using your own certificates](./tls-termination-using-own-certificates.md)\n* [The resources loader](./resources-loader.md)\n* [Log levels customization](./custom-log-levels.md)\n* [Initial state customization](./custom-initial-state.md)\n* [Communicate with Kafka](./communicate-with-kafka.md)\n* [Create your custom Authentication module](./create-custom-auth-module.md)\n* [Working with Eureka](./working-with-eureka.md)\n* [Instantiate a WAF with Coraza](./instantiate-waf-coraza.md)\n* [Zip Backend plugin](./zip-backend-plugin.md) \n@@@\n"},{"name":"instantiate-waf-coraza.md","id":"/how-to-s/instantiate-waf-coraza.md","url":"/how-to-s/instantiate-waf-coraza.html","title":"Instantiate a WAF with Coraza","content":"# Instantiate a WAF with Coraza\n\n
      \nRoute plugins:\nCoraza WAF\nOverride Host Header\n
      \n\nSometimes you may want to secure an app with a [Web Appplication Firewall (WAF)](https://en.wikipedia.org/wiki/Web_application_firewall) and apply the security rules from the [OWASP Core Rule Set](https://owasp.org/www-project-modsecurity-core-rule-set/). To allow that, we integrated [the Coraza WAF](https://coraza.io/) in Otoroshi through a plugin that uses the WASM version of Coraza.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a WAF configuration\n\nfirst, go on [the features page of otoroshi](http://otoroshi.oto.tools:8080/bo/dashboard/features) and then click on the [Coraza WAF configs. item](http://otoroshi.oto.tools:8080/bo/dashboard/extensions/coraza-waf/coraza-configs). \n\nNow create a new configuration, give it a name and a description, ensure that you enabled the `Inspect req/res body` flag and save your configuration.\n\nThe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Configure Coraza and the OWASP Core Rule Set\n\nNow you can easily configure the coraza WAF in the `json` config. section. By default it should look something like :\n\n```json\n{\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRuleEngine DetectionOnly\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n}\n```\n\nYou can find anything about it in [the documentation of Coraza](https://coraza.io/docs/tutorials/introduction/).\n\nhere we have the basic setup to apply the OWASP core rule set in detection mode only. \nSo each time Coraza will find something weird in a request, it will only log it but let the request pass.\n We can enable blocking by setting `\"SecRuleEngine On\"`\n\nwe can also deny access to the `/admin` uri by adding the following directive\n\n```json\n\"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\"\n```\n\nYou can also provide multiple profile of rules in the `directives_map` with different names and use the `per_authority_directives` object to map hostnames to a specific profile.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/apis/coraza-waf.extensions.otoroshi.io/v1/coraza-configs/coraza-waf-demo' \\\n -u admin-api-apikey-id:admin-api-apikey-secret -H 'Content-Type: application/json' -d '\n{\n \"id\": \"coraza-waf-demo\",\n \"name\": \"My blocking WAF\",\n \"description\": \"An awesome WAF\",\n \"inspect_body\": true,\n \"config\": {\n \"directives_map\": {\n \"default\": [\n \"Include @recommended-conf\",\n \"Include @crs-setup-conf\",\n \"Include @owasp_crs/*.conf\",\n \"SecRule REQUEST_URI \\\"@streq /admin\\\" \\\"id:101,phase:1,t:lowercase,deny\\\"\",\n \"SecRuleEngine On\"\n ]\n },\n \"default_directives\": \"default\",\n \"per_authority_directives\": {}\n }\n}'\n```\n\n### Add the WAF plugin on your route\n\nNow you can create a new route that will use your WAF configuration. Let say we want a route on `http://wouf.oto.tools:8080` to goes to `https://www.otoroshi.io`. Now add the `Coraza WAF` plugin to your route and in the configuration select the configuration you created previously.\n\nthe corresponding admin api call is the following :\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n -H 'Content-Type: application/json' -d '\n{\n \"id\": \"route_demo\",\n \"name\": \"WAF route\",\n \"description\": \"A new route with a WAF enabled\",\n \"frontend\": {\n \"domains\": [\n \"wouf.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"www.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.wasm.proxywasm.NgCorazaWAF\",\n \"config\": {\n \"ref\": \"coraza-waf-demo\"\n },\n \"plugin_index\": {\n \"validate_access\": 0,\n \"transform_request\": 0,\n \"transform_response\": 0\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"plugin_index\": {\n \"transform_request\": 1\n }\n }\n ]\n}'\n```\n\n### Try to use an exploit ;)\n\nlet try to trigger Coraza with a Log4Shell crafted request:\n\n```sh\ncurl 'http://wouf.oto.tools:9999' -H 'foo: ${jndi:rmi://foo/bar}' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nor access to `/admin`\n\n```sh\ncurl 'http://wouf.oto.tools:9999/admin' --include\n\nHTTP/1.1 403 Forbidden\nDate: Thu, 25 May 2023 09:47:04 GMT\nContent-Type: text/plain\nContent-Length: 0\n\n```\n\nif you look at otoroshi logs you will find something like :\n\n```log\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell \n [file \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\"] [line \"10608\"] [id \"944150\"] [rev \"\"] \n [msg \"Potential Remote Command Execution: Log4j / Log4shell\"] [data \"\"] [severity \"critical\"] \n [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] [tag \"application-multi\"] \n [tag \"language-java\"] [tag \"platform-multi\"] [tag \"attack-rce\"] [tag \"OWASP_CRS\"] \n [tag \"capec/1000/152/137/6\"] [tag \"PCI/6.5.2\"] [tag \"paranoia-level/1\"] [hostname \"wwwwouf.oto.tools\"] \n [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. Inbound Anomaly Score Exceeded (Total Score: 5) \n [file \"@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf\"] [line \"11029\"] [id \"949110\"] [rev \"\"] \n [msg \"Inbound Anomaly Score Exceeded (Total Score: 5)\"] \n [data \"\"] [severity \"emergency\"] [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] \n [tag \"anomaly-evaluation\"] [hostname \"wwwwouf.oto.tools\"] [uri \"/\"] [unique_id \"uTYakrlgMBydVGLodbz\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"uTYakrlgMBydVGLodbz\" context_id=3 action=\"deny\" phase=\"http_response_headers\"\n...\n[error] otoroshi-proxy-wasm - [client \"127.0.0.1\"] Coraza: Warning. [file \"\"] [line \"12914\"] \n [id \"101\"] [rev \"\"] [msg \"\"] [data \"\"] [severity \"emergency\"] [ver \"\"] [maturity \"0\"] [accuracy \"0\"] \n [hostname \"wwwwouf.oto.tools\"] [uri \"/admin\"] [unique_id \"mqXZeMdzRaVAqIiqvHf\"]\n[info] otoroshi-proxy-wasm - Transaction interrupted tx_id=\"mqXZeMdzRaVAqIiqvHf\" context_id=2 action=\"deny\" phase=\"http_request_headers\"\n```\n\n### Generated events\n\neach time Coraza will generate log about vunerability detection, an event will be generated in otoroshi and exporter through the usual data exporter way. The event will look like :\n\n```json\n{\n \"@id\" : \"86b647450-3cc7-42a9-aaec-828d261a8c74\",\n \"@timestamp\" : 1684938211157,\n \"@type\" : \"CorazaTrailEvent\",\n \"@product\" : \"otoroshi\",\n \"@serviceId\" : \"--\",\n \"@service\" : \"--\",\n \"@env\" : \"prod\",\n \"level\" : \"ERROR\",\n \"msg\" : \"Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell\",\n \"fields\" : {\n \"hostname\" : \"wouf.oto.tools\",\n \"maturity\" : \"0\",\n \"line\" : \"10608\",\n \"unique_id\" : \"oNbisKlXWaCdXntaUpq\",\n \"tag\" : \"paranoia-level/1\",\n \"data\" : \"\",\n \"accuracy\" : \"0\",\n \"uri\" : \"/\",\n \"rev\" : \"\",\n \"id\" : \"944150\",\n \"client\" : \"127.0.0.1\",\n \"ver\" : \"OWASP_CRS/4.0.0-rc1\",\n \"file\" : \"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\",\n \"msg\" : \"Potential Remote Command Execution: Log4j / Log4shell\",\n \"severity\" : \"critical\"\n },\n \"raw\" : \"[client \\\"127.0.0.1\\\"] Coraza: Warning. Potential Remote Command Execution: Log4j / Log4shell [file \\\"@owasp_crs/REQUEST-944-APPLICATION-ATTACK-JAVA.conf\\\"] [line \\\"10608\\\"] [id \\\"944150\\\"] [rev \\\"\\\"] [msg \\\"Potential Remote Command Execution: Log4j / Log4shell\\\"] [data \\\"\\\"] [severity \\\"critical\\\"] [ver \\\"OWASP_CRS/4.0.0-rc1\\\"] [maturity \\\"0\\\"] [accuracy \\\"0\\\"] [tag \\\"application-multi\\\"] [tag \\\"language-java\\\"] [tag \\\"platform-multi\\\"] [tag \\\"attack-rce\\\"] [tag \\\"OWASP_CRS\\\"] [tag \\\"capec/1000/152/137/6\\\"] [tag \\\"PCI/6.5.2\\\"] [tag \\\"paranoia-level/1\\\"] [hostname \\\"wouf.oto.tools\\\"] [uri \\\"/\\\"] [unique_id \\\"oNbisKlXWaCdXntaUpq\\\"]\\n\",\n}\n```"},{"name":"resources-loader.md","id":"/how-to-s/resources-loader.md","url":"/how-to-s/resources-loader.html","title":"The resources loader","content":"# The resources loader\n\nThe resources loader is a tool to create an Otoroshi resource from a raw content. This content can be found on each Otoroshi resources pages (services descriptors, apikeys, certificates, etc ...). To get the content of a resource as file, you can use the two export buttons, one to export as JSON format and the other as YAML format.\n\nOnce exported, the content of the resource can be import with the resource loader. You can import single or multiples resources on one time, as JSON and YAML format.\n\nThe resource loader is available on this route [`bo/dashboard/resources-loader`](http://otoroshi.oto.tools:8080/bo/dashboard/resources-loader).\n\nOn this page, you can paste the content of your resources and click on **Load resources**.\n\nFor each detected resource, the loader will display :\n\n* a resource name corresponding to the field `name` \n* a resource type corresponding to the type of created resource (ServiceDescriptor, ApiKey, Certificate, etc)\n* a toggle to choose if you want to include the element for the creation step\n* the updated status by the creation process\n\nOnce you have selected the resources to create, you can **Import selected resources**.\n\nOnce generated, all status will be updated. If all is working, the status will be equals to done.\n\nIf you want to get back to the initial page, you can use the **restart** button."},{"name":"secure-an-app-with-jwt-verifiers.md","id":"/how-to-s/secure-an-app-with-jwt-verifiers.md","url":"/how-to-s/secure-an-app-with-jwt-verifiers.html","title":"Secure an api with jwt verifiers","content":"# Secure an api with jwt verifiers\n\n
      \nRoute plugins:\nJwt verification only\n
      \n\nA Jwt verifier is the guard that verifies the signature of tokens in requests. \n\nA verifier can obvisouly verify or generate.\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Your first jwt verifier\n\nLet's start by validating all incoming request tokens tokens on our simple route created in the @ref:[Before you start](#before-you-start) section.\n\n1. Navigate to the simple route\n2. Search in the list of plugins and add the `Jwt verification only` plugin on the flow\n3. Click on `Start by select or create a JWT Verifier`\n4. Create a new JWT verifier\n5. Set `simple-jwt-verifier` as `Name`\n6. Select `Hmac + SHA` as `Algo` (for this example, we expect tokens with a symetric signature), `512` as `SHA size` and `otoroshi` as `HMAC secret`\n7. Confirm the creation \n\nSave your route and try to call it\n\n```sh\ncurl -X GET 'http://myservice.oto.tools:8080/' --include\n```\n\nThis should output : \n```json\n{\n \"Otoroshi-Error\": \"error.expected.token.not.found\"\n}\n```\n\nA simple way to generate a token is to use @link:[jwt.io](http://jwt.io) { open=new }. Once navigate, define `HS512` as `alg` in header section and insert `otoroshi` as verify signature secret. \n\nOnce created, copy-paste the token from jwt.io to the Authorization header and call our service.\n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"X-JWT-Token: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `X-JWT-Token` in headers field. Its value is exactly the same as the passed token.\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.otoroshi.io\",\n \"X-JWT-Token\": \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ipDFgkww51mSaSg_199BMRj4gK20LGz_czozu3u8rCFFO1X20MwcabSqEzUc0q4qQ4rjTxjoR4HeUDVcw8BxoQ\",\n ...\n }\n}\n```\n\n### Verify and generate a new token\n\nAn other feature is to verify the incomings tokens and generate new ones, with a different signature and claims. \n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify and re-sign JWT token`. \n2. Edit the name with `jwt-verify-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Save your verifier.\n\n> Note : the name of the verifier doesn't impact the identifier. So you can save the changes of your verifier without modifying the identifier used in your call. \n\n```sh\n# replace xxxx by the generated token\ncurl -X GET \\\n -H \"Authorization: xxxx\" \\\n 'http://myservice.oto.tools:8080'\n```\n\nThis should output a json with `authorization` in headers field. This time, the value are different and you can check his signature on @link:[jwt.io](https://jwt.io) { open=new } (the expected secret of the generated token is **otoroshi-internal-secret**)\n\n\n\n### Verify, transform and generate a new token\n\nThe most advanced verifier is able to do the same as the previous ones, with the ability to configure the token generation (claims, output header name).\n\nLet's start by extending the @link:[previous verifier](http://otoroshi.oto.tools:8080/bo/dashboard/jwt-verifiers) { open=new }.\n\n1. Jump to the `Verif Strategy` field and select `Verify, transform and re-sign JWT token`. \n\n2. Edit the name with `jwt-verify-transform-and-resign`\n3. Remove the default field in `Verify token fields` array\n4. Change the second `Hmac secret` in `Re-sign settings` section with `otoroshi-internal-secret`\n5. Set `Internal-Authorization` as `Header name`\n6. Set `key` on first field of `Rename token fields` and `from-otoroshi-verifier` on second field\n7. Set `generated-key` and `generated-value` as `Set token fields`\n8. Add `generated_at` and `${date}` as second field of `Set token fields` (Otoroshi supports an @ref:[expression language](../topics/expression-language.md))\n9. Save your verifier and try to call your service again.\n\nThis should output a json with `authorization` in headers field and our generate token in `Internal-Authorization`.\nOnce paste in @link:[jwt.io](https://jwt.io) { open=new }, you should have :\n\n\n\nYou can see, in the payload of your token, the two claims **from-otoroshi-verifier** and **generated-key** added during the generation of the token by the JWT verifier.\n"},{"name":"secure-app-with-auth0.md","id":"/how-to-s/secure-app-with-auth0.md","url":"/how-to-s/secure-app-with-auth0.html","title":"Secure an app with Auth0","content":"# Secure an app with Auth0\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Configure an Auth0 client\n\nThe first step of this tutorial is to setup an Auth0 application with the information of the instance of our Otoroshi.\n\nNavigate to @link:[https://manage.auth0.com](https://manage.auth0.com) { open=new } (create an account if it's not already done). \n\nLet's create an application when clicking on the **Applications** button on the sidebar. Then click on the **Create application** button on the top right.\n\n1. Choose `Regular Web Applications` as `Application type`\n2. Then set for example `otoroshi-client` as `Name`, and confirm the creation\n3. Jump to the `Settings` tab\n4. Scroll to the `Application URLs` section and add the following url as `Allowed Callback URLs` : `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n5. Set `https://otoroshi.oto.tools:8080/` as `Allowed Logout URLs`\n6. Set `https://otoroshi.oto.tools:8080` as `Allowed Web Origins` \n7. Save changes at the bottom of the page.\n\nOnce done, we have a full setup, with a client ID and secret at the top of the page, which authorizes our Otoroshi and redirects the user to the callback url when they log into Auth0.\n\n### Create an Auth0 provider module\n\nLet's back to Otoroshi to create an authentication module with `OAuth2 / OIDC provider` as `type`.\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth provider` in the type selector field\n3. Then click on `Get from OIDC config` and paste `https://..auth0.com/.well-known/openid-configuration`. Replace the tenant name by the name of your tenant (displayed on the left top of auth0 page), and the region of the tenant (`eu` in my case).\n\nOnce done, set the `Client ID` and the `Client secret` from your Auth0 application. End the configuration with `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`.\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs).\n\n### Connect to Otoroshi with Auth0 authentication\n\nTo secure Otoroshi with your Auth0 configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n2. Scroll to the **BackOffice auth. settings**\n3. Select your last Authentication configuration (created in the previous section)\n4. Save the global configuration with the button on the top right\n\n#### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the *Login using third-party* button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the auth0 server login page\n4. Set your account credentials\n5. Good works! You're connected to Otoroshi with an Auth0 module.\n\n### Secure an app with Auth0 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new **Authentication module** configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a connected user by a third-party server is covered by another route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called **a private app** when it is protected by an Authentication module.\n\nWe can set the Authentication module on your route.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n5. Now you can try to call your route and see the Auth0 login page appears.\n\n\n"},{"name":"secure-app-with-keycloak.md","id":"/how-to-s/secure-app-with-keycloak.md","url":"/how-to-s/secure-app-with-keycloak.html","title":"Secure an app with Keycloak","content":"# Secure an app with Keycloak\n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running a keycloak instance with docker\n\n```sh\ndocker run \\\n -p 8080:8080 \\\n -e KEYCLOAK_USER=admin \\\n -e KEYCLOAK_PASSWORD=admin \\\n --name keycloak-server \\\n --detach jboss/keycloak:15.0.1\n```\n\nThis should download the image of keycloak (if you haven't already it) and display the digest of the created container. This command mapped TCP port 8080 in the container to port 8080 of your laptop and created a server with `admin/admin` as admin credentials.\n\nOnce started, you can open a browser on @link:[http://localhost:8080](http://localhost:8080) { open=new } and click on `Administration Console`. Log to your instance with `admin/admin` as credentials.\n\nThe first step is to create a Keycloak client, an entity that can request Keycloak to authenticate a user. Click on the **clients** button on the sidebar, and then on **Create** button at the top right of the view.\n\nFill the client form with the following values.\n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Protocol`: `openid-connect`\n* `Root URL`: `http://otoroshi.oto.tools:8080/`\n\nValidate the creation of the client by clicking on the **Save** button.\n\nThe next step is to change the `Access Type` used by default. Jump to the `Access Type` field and select `confidential`. The confidential configuration force the client application to send at Keycloak a client ID and a client Secret. Scroll to the bottom of the page and save the configuration.\n\nNow scroll to the top of your page. Just at the right of the `Settings` tab, a new tab appeared : the `Credentials` page. Click on this tab, and make sure that `Client Id and Secret` is selected as `Client Authenticator` and copy the generated `Secret` to the next part.\n\n### Create a Keycloak provider module\n\n1. Go ahead, and navigate to http://otoroshi.oto.tools:8080\n1. Click on the cog icon on the top right\n1. Then `Authentication configs` button\n1. And add a new configuration when clicking on the `Add item` button\n2. Select the `OAuth2 / OIDC provider` in the type selector field\n3. Set a basic name and description\n\nA simple way to import a Keycloak client is to give the `URL of the OpenID Connect` Otoroshi. By default, keycloak used the next URL : `http://localhost:8080/auth/realms/master/.well-known/openid-configuration`. \n\nClick on the `Get from OIDC config` button and paste the previous link. Once it's done, scroll to the `URLs` section. All URLs has been fill with the values picked from the JSON object returns by the previous URL.\n\nThe only fields to change are : \n\n* `Client ID`: `keycloak-otoroshi-backoffice`\n* `Client Secret`: Paste the secret from the Credentials Keycloak page. In my case, it's something like `90c9bf0b-2c0c-4eb0-aa02-72195beb9da7`\n* `Callback URL`: `http://otoroshi.oto.tools:8080/backoffice/auth0/callback`\n\nAt the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs). Nothing else to change, just save the configuration.\n\n### Connect to Otoroshi with Keycloak authentication\n\nTo secure Otoroshi with your Keycloak configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new })\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the keycloak login page\n4. Set `admin/admin` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an Keycloak module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Visualize an admin user session or a private user session\n\nEach user, wheter connected user to the Otoroshi UI or at a private Otoroshi app, has an own session. As an administrator of Otoroshi, you can visualize via Otoroshi the list of the connected users and their profile.\n\nLet's start by navigating to the `Admin users sessions` page (just @link:[here](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/admin) or when clicking on the cog, and on the `Admins sessions` button at the bottom of the list).\n\nThis page gives a complete view of the connected admins. For each admin, you have his connection date and his expiration date. You can also check the `Profile` and the `Rights` of the connected users.\n\nIf we check the profile and the rights of the previously logged user (from Keycloak in the previous part) we can retrieve the following information :\n\n```json\n{\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"upn\": \"admin\",\n \"email_verified\": false,\n \"address\": {},\n \"groups\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ],\n \"preferred_username\": \"admin\"\n}\n```\n\nand his default rights \n\n```sh\n[\n {\n \"tenant\": \"default:rw\",\n \"teams\": [\n \"default:rw\"\n ]\n }\n]\n```\n\nWe haven't create any specific groups in Keycloak or specify rights in Otoroshi for him. In this case, the use received the default Otoroshi rights at his connection. The user can navigate on the default Organization and Teams (which are two resources created by Otoroshi at the boot) and have the full access on its (`r`: Read, `w`: Write, `*`: read/write).\n\nIn the same way, you'll find all users connected to a private Otoroshi app when navigate on the @link:[`Private App View`](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) or using the cog at the top of the page. \n\n### Configure the Keycloak module to force logged in users to be an Otoroshi admin with full access\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Supers admin only` button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, click on the cog button, and check that you have access to the full features of Otoroshi (like Admin user sessions). Now, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n]\n```\n\n### Merge Id token content on user profile\n\nGo back to the Keycloak module in `Authentication configs` view. Turn on the `Read profile` from token button and save your configuration. Try again the connection to Otoroshi using Keycloak third-party server.\n\nOnce connected, your profile should be contains all Keycloak id token : \n```json\n{\n \"exp\": 1634286674,\n \"iat\": 1634286614,\n \"auth_time\": 1634286614,\n \"jti\": \"eb368578-e886-4caa-a51b-c1d04973c80e\",\n \"iss\": \"http://localhost:8080/auth/realms/master\",\n \"aud\": [\n \"master-realm\",\n \"account\"\n ],\n \"sub\": \"4c8cd101-ca28-4611-80b9-efa504ac51fd\",\n \"typ\": \"Bearer\",\n \"azp\": \"keycloak-otoroshi-backoffice\",\n \"session_state\": \"e44fe471-aa3b-477d-b792-4f7b4caea220\",\n \"acr\": \"1\",\n \"allowed-origins\": [\n \"http://otoroshi.oto.tools:8080\"\n ],\n \"realm_access\": {\n \"roles\": [\n \"create-realm\",\n \"default-roles-master\",\n \"offline_access\",\n \"admin\",\n \"uma_authorization\"\n ]\n },\n \"resource_access\": {\n \"master-realm\": {\n \"roles\": [\n \"view-identity-providers\",\n \"view-realm\",\n \"manage-identity-providers\",\n \"impersonation\",\n \"create-client\",\n \"manage-users\",\n \"query-realms\",\n \"view-authorization\",\n \"query-clients\",\n \"query-users\",\n \"manage-events\",\n \"manage-realm\",\n \"view-events\",\n \"view-users\",\n \"view-clients\",\n \"manage-authorization\",\n \"manage-clients\",\n \"query-groups\"\n ]\n },\n \"account\": {\n \"roles\": [\n \"manage-account\",\n \"manage-account-links\",\n \"view-profile\"\n ]\n }\n }\n ...\n}\n```\n\n### Manage the Otoroshi user rights from keycloak\n\nOne powerful feature supports by Otoroshi, is to use the Keycloak groups attributes to set a list of rights for a Otoroshi user.\n\nIn the Keycloak module, you have a field, named `Otoroshi rights field name` with `otoroshi_rights` as default value. This field is used by Otoroshi to retrieve information from the Id token groups.\n\nLet's create a group in Keycloak, and set our default Admin user inside.\nIn Keycloak admin console :\n\n1. Navigate to the groups view, using the keycloak sidebar\n2. Create a new group with `my-group` as `Name`\n3. Then, on the `Attributes` tab, create an attribute with `otoroshi_rights` as `Key` and the following json array as `Value`\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\nWith this configuration, the user have a full access on all Otoroshi resources (my-future-team is not created in Otoroshi but it's not a problem, Otoroshi can handle it and use this rights only when the team will be present)\n\nClick on the **Add** button and **save** the group. The last step is to assign our user to this group. Jump to `Users` view using the sidebar, click on **View all users**, edit the user and his group membership using the `Groups` tab (use **join** button the assign user in `my-group`).\n\nThe next step is to add a mapper in the Keycloak client. By default, Keycloak doesn't expose any users information (like group membership or users attribute). We need to ask to Keycloak to expose the user attribute `otoroshi_rights` set previously on group.\n\nNavigate to the `Keycloak-otoroshi-backoffice` client, and jump to `Mappers` tab. Create a new mapper with the following values: \n\n* Name: `otoroshi_rights`\n* Mapper Type: `User Attribute`\n* User Attribute: `otoroshi_rights`\n* Token Claim Name: `otoroshi_rights`\n* Claim JSON Type: `JSON`\n* Multivalued: `√`\n* Aggregate attribute values: `√`\n\nGo back to the Authentication Keycloak module inside Otoroshi UI, and turn off **Super admins only**. **Save** the configuration.\n\nOnce done, try again the connection to Otoroshi using Keycloak third-party server.\nNow, your rights should be : \n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```\n\n### Secure an app with Keycloak authentication\n\nThe only change to apply on the previous authentication module is on the callback URL. When you want secure a Otoroshi service, and transform it on `Private App`, you need to set the `Callback URL` at `http://privateapps.oto.tools:8080/privateapps/generic/callback`. This configuration will redirect users to the backend service after they have successfully logged in.\n\n1. Go back to the authentication module\n2. Jump to the `Callback URL` field\n3. Paste this value `http://privateapps.oto.tools:8080/privateapps/generic/callback`\n4. Save your configuration\n5. Navigate to `http://myservice.oto.tools:8080`.\n6. You should redirect to the keycloak login page.\n7. Once logged in, you can check the content of the private app session created.\n\nThe rights should be : \n\n```json\n[\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\",\n \"my-future-team:rw\"\n ]\n }\n]\n```"},{"name":"secure-app-with-ldap.md","id":"/how-to-s/secure-app-with-ldap.md","url":"/how-to-s/secure-app-with-ldap.html","title":"Secure an app and/or your Otoroshi UI with LDAP","content":"# Secure an app and/or your Otoroshi UI with LDAP\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n#### Running an simple OpenLDAP server \n\nRun OpenLDAP docker image : \n```sh\ndocker run \\\n -p 389:389 \\\n -p 636:636 \\\n --env LDAP_ORGANISATION=\"Otoroshi company\" \\\n --env LDAP_DOMAIN=\"otoroshi.tools\" \\\n --env LDAP_ADMIN_PASSWORD=\"otoroshi\" \\\n --env LDAP_READONLY_USER=\"false\" \\\n --env LDAP_TLS\"false\" \\\n --env LDAP_TLS_ENFORCE\"false\" \\\n --name my-openldap-container \\\n --detach osixia/openldap:1.5.0\n```\n\nLet's make the first search in our LDAP container :\n\n```sh\ndocker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=otoroshi,dc=tools -D \"cn=admin,dc=otoroshi,dc=tools\" -w otoroshi\n```\n\nThis should output :\n```sh\n# extended LDIF\n ...\n# otoroshi.tools\ndn: dc=otoroshi,dc=tools\nobjectClass: top\nobjectClass: dcObject\nobjectClass: organization\no: Otoroshi company\ndc: otoroshi\n\n# search result\nsearch: 2\nresult: 0 Success\n...\n```\n\nNow you can seed the open LDAP server with a few users. \n\nJoin your LDAP container.\n\n```sh\ndocker exec -it my-openldap-container \"/bin/bash\"\n```\n\nThe command `ldapadd` needs of a file to run.\n\nLaunch this command to create a `bootstrap.ldif` with one organization, one singers group with John user and a last group with Baz as scientist.\n\n```sh\necho -e \"\ndn: ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: People\n\ndn: ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: organizationalUnit\nou: Role\n\ndn: uid=john,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: john\ncn: John\nsn: Brown\nmail: john@otoroshi.tools\npostalCode: 88442\nuserPassword: password\n\ndn: uid=baz,ou=People,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nuid: baz\ncn: Baz\nsn: Wilson\nmail: baz@otoroshi.tools\npostalCode: 88443\nuserPassword: password\n\ndn: cn=singers,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: singers\nmember: uid=john,ou=People,dc=otoroshi,dc=tools\n\ndn: cn=scientists,ou=Role,dc=otoroshi,dc=tools\nobjectclass: top\nobjectclass: groupOfNames\ncn: scientists\nmember: uid=baz,ou=People,dc=otoroshi,dc=tools\n\" > bootstrap.ldif\n\nldapadd -x -w otoroshi -D \"cn=admin,dc=otoroshi,dc=tools\" -f bootstrap.ldif -v\n```\n\n### Create an Authentication configuration\n\n- Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n- Click on the cog icon on the top right\n- Then `Authentication configs` button\n- And add a new configuration when clicking on the `Add item` button\n- Select the `Ldap auth. provider` in the type selector field\n- Set a basic name and description\n- Then set `ldap://localhost:389` as `LDAP Server URL`and `dc=otoroshi,dc=tools` as `Search Base`\n- Create a group filter (in the next part, we'll change this filter to spread users in different groups with given rights) with \n - objectClass=groupOfNames as `Group filter` \n - All as `Tenant`\n - All as `Team`\n - Read/Write as `Rights`\n- Set the search filter as `(uid=${username})`\n- Set `cn=admin,dc=otoroshi,dc=tools` as `Admin username`\n- Set `otoroshi` as `Admin password`\n- At the bottom of the page, disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n\n At this point, your configuration should be similar to :\n \n\n\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n- Test the connection when clicking on `Test admin connection` button. This should show a `It works!` message\n\n- Finally, test the user connection button and set `john/password` or `baz/password` as credentials. This should show a `It works!` message\n\n> Dont' forget to save on the bottom page your configuration before to quit the page.\n\n\n### Connect to Otoroshi with LDAP authentication\n\nTo secure Otoroshi with your LDAP configuration, we have to register an **Authentication configuration** as a BackOffice Auth. configuration.\n\n- Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n- Scroll to the **BackOffice auth. settings**\n- Select your last Authentication configuration (created in the previous section)\n- Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n- Disconnect from your instance\n- Then click on the **Login using third-party** button (or navigate to @link:[http://otoroshi.oto.tools:8080/backoffice/auth0/login](http://otoroshi.oto.tools:8080/backoffice/auth0/login) { open=new })\n- Set `john/password` or `baz/password` as credentials\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n\n#### Secure an app with LDAP authentication\n\nOnce the configuration is done, you can secure any of Otoroshi routes. \n\n- Navigate to any created route\n- Add the `Authentication` plugin to your route\n- Select your Authentication config inside the list\n- Save your configuration\n\nNow try to call your route. The login module should appear.\n\n#### Manage LDAP users rights on Otoroshi\n\nFor each group filter, you can affect a list of rights:\n\n- on an `Organization`\n- on a `Team`\n- and a level of rights : `Read`, `Write` or `Read/Write`\n\n\nStart by navigate to your authentication configuration (created in @ref:[previous](#create-an-authentication-configuration) step).\n\nThen, replace the values of the `Mapping group filter` field to match LDAP groups with Otoroshi rights.\n\n\n\n\nWith this configuration, Baz is an administrator of Otoroshi with full rights (read / write) on all organizations.\n\nConversely, John can't see any configuration pages (like the danger zone) because he has only the read rights on Otoroshi.\n\nYou can easily test this behaviour by @ref:[testing](#testing-your-configuration) with both credentials.\n\n\n#### Advanced usage of LDAP Authentication\n\nIn the previous section, we have define rights for each LDAP groups. But in some case, we want to have a finer granularity like set rights for a specific user. The last 4 fields of the authentication form cover this. \n\nLet's start by adding few properties for each connected users with `Extra metadata`.\n\n```json\n// Add this configuration in extra metadata part\n{\n \"provider\": \"OpenLDAP\"\n}\n```\n\nThe next field `Data override` is merged with extra metadata when a user connects to a `private app` or to the UI (inside Otoroshi, private app is a service secure by any authentication module). The `Email field name` is configured to match with the `mail` field from LDAP user data.\n\n```json \n{\n \"john@otoroshi.tools\": {\n \"stage_name\": \"Will\"\n }\n}\n```\n\nIf you try to connect to an app with this configuration, the user result profile should be :\n\n```json\n{\n ...,\n \"metadata\": {\n \"lastname\": \"Willy\",\n \"stage_name\": \"Will\"\n }\n}\n```\n\nLet's try to increase the John rights with the `Additional rights group`.\n\nThis field supports the creation of virtual groups. A virtual group is composed of a list of users and a list of rights for each teams/organizations.\n\n```json\n// increase_john_rights is a virtual group which adds full access rights at john \n{\n \"increase_john_rights\": {\n \"rights\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ],\n \"users\": [\n \"john@otoroshi.tools\"\n ]\n }\n}\n```\n\nThe last field `Rights override` is useful when you want erase the rights of an user with only specific rights. This field is the last to be applied on the user rights. \n\nTo resume, when John connects to Otoroshi, he receives the rights to only read the default Organization (from **Mapping group filter**), then he is promote to administrator role (from **Additional rights group**) and finally his rights are reset with the last field **Rights override** to the read rights.\n\n```json \n{\n \"john@otoroshi.tools\": [\n {\n \"tenant\": \"*:r\",\n \"teams\": [\n \"*:r\"\n ]\n }\n ]\n}\n```\n\n\n\n\n\n\n\n\n"},{"name":"secure-the-communication-between-a-backend-app-and-otoroshi.md","id":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.md","url":"/how-to-s/secure-the-communication-between-a-backend-app-and-otoroshi.html","title":"Secure the communication between a backend app and Otoroshi","content":"# Secure the communication between a backend app and Otoroshi\n\n
      \nRoute plugins:\nOtoroshi challenge token\n
      \n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nLet's create a new route with the Otorochi challenge plugin enabled.\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myapi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 8081,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiChallenge\",\n \"config\": {\n \"version\": 2,\n \"ttl\": 30,\n \"request_header_name\": \"Otoroshi-State\",\n \"response_header_name\": \"Otoroshi-State-Resp\",\n \"algo_to_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"algo_from_backend\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"secret\",\n \"base64\": false\n },\n \"state_resp_leeway\": 10\n }\n }\n ]\n}\nEOF\n```\n\nLet's use the following application, developed in NodeJS, which supports both versions of the exchange protocol.\n\nClone this @link:[repository](https://github.com/MAIF/otoroshi/blob/master/demos/challenge) and run the installation of the dependencies.\n\n```sh\ngit clone 'git@github.com:MAIF/otoroshi.git' --depth=1\ncd ./otoroshi/demos/challenge\nnpm install\nPORT=8081 node server.js\n```\n\nThe last command should return : \n\n```sh\nchallenge-verifier listening on http://0.0.0.0:8081\n```\n\nThis project runs an express client with one middleware. The middleware handles each request, and check if the header `State token header` is present in headers. By default, the incoming expected header is `Otoroshi-State` by the application and `Otoroshi-State-Resp` header in the headers of the return request. \n\nTry to call your service via http://myapi.oto.tools:8080/. This should return a successful response with all headers received by the backend app. \n\nNow try to disable the middleware in the nodejs file by commenting the following line. \n\n```js\n// app.use(OtoroshiMiddleware());\n```\n\nTry to call again your service. This time, Otoroshi breaks the return response from your backend service, and returns.\n\n```sh\nDownstream microservice does not seems to be secured. Cancelling request !\n```"},{"name":"secure-with-apikey.md","id":"/how-to-s/secure-with-apikey.md","url":"/how-to-s/secure-with-apikey.html","title":"Secure an api with api keys","content":"# Secure an api with api keys\n\n
      \nRoute plugins:\nApikeys\n
      \n\n### Before you start\n\n@@include[fetch-and-start.md](../includes/fetch-and-start.md) { #init }\n\n### Create a simple route\n\n**From UI**\n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/routes](http://otoroshi.oto.tools:8080/bo/dashboard/routes) { open=new } and click on the `create new route` button\n2. Give a name to your route\n3. Save your route\n4. Set `myservice.oto.tools` as frontend domains\n5. Set `https://mirror.otoroshi.io` as backend target (hostname: `mirror.otoroshi.io`, port: `443`, Tls: `Enabled`)\n\n**From Admin API**\n\n```sh\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n### Secure routes with api key\n\nBy default, a route is public. In our case, we want to secure all paths starting with `/api` and leave all others unauthenticated.\n\nLet's add a new plugin, called `Apikeys`, to our route. Search in the list of plugins, then add it to the flow.\nOnce done, restrict its range by setting up `/api` in the `Informations>include` section.\n\n**From Admin API**\n\n```sh\ncurl -X PUT http://otoroshi-api.oto.tools:8080/api/routes/myservice \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"myservice\",\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/api\"\n ],\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n }\n }\n ]\n}\nEOF\n```\n\nNavigate to @link:[http://myservice.oto.tools:8080/api/test](http://myservice.oto.tools:8080/api/test) { open=new } again. If the service is configured, you should have a `Service Not found error`.\n\nThe expected error on the `/api/test`, indicate that an api key is required to access to this part of the backend service.\n\nNavigate to any other routes which are not starting by `/api/*` like @link:[http://myservice.oto.tools:8080/test/bar](http://myservice.oto.tools:8080/test/bar) { open=new }\n\n\n### Generate an api key to request secure services\n\nNavigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add](http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add) { open=new } or when clicking on the **Add apikey** button on the sidebar.\n\nThe only required fields of an Otoroshi api key are : \n\n* `ApiKey id`\n* `ApiKey Secret`\n* `ApiKey Name`\n\nThese fields are automatically generated by Otoroshi. However, you can override these values and indicate an additional description.\n\nTo simplify the rest of the tutorial, set the values:\n\n* `my-first-api-key-id` as `ApiKey Id`\n* `my-first-api-key-secret` as `ApiKey Secret`\n\nClick on **Create and stay on this ApiKey** button at the bottom of the page.\n\nNow you created the key, it's time to call our previous generated service with it.\n\nOtoroshi supports 4 methods to achieve that: \n\nFirst one by passing Otoroshi api key in two headers : `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` (these headers names can be override on each service).\nThe second by passing Otoroshi api key in the authentication Header (basically the `Authorization` header) as a basic encoded value. The third option is to use the bearer generated for your apikey (you can get it by calling `curl http://otoroshi-api.oto.tools:8080/api/apikeys/my-first-api-key-id/bearer`). A fourth option is to use jwt token but we will not review it here but you can find a @ref[tutorial here](./secure-with-oauth2-client-credentials.md).\n\nLet's go ahead and call our service with the first method :\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the second method using basic authentication:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nor\n\n```sh\ncurl -X GET \\\n -u my-first-api-key-id:my-first-api-key-secret \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nthen with the third method using otoroshi bearer:\n\n```sh\ncurl -X GET \\\n -H 'Authorization: Bearer otoapk_my-first-api-key-id_99cb8e081d692044593ad0e658a67a5114f7afbdcbeb26f8087cce0df3b610b2' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\n> Tips : To easily fill your headers, you can jump to the `Call examples` section in each api key view. In this section the header names are the default values and the service url is not set. You have to adapt these lines to your case. \n\n### Override defaults headers names for a route\n\nIn some case, we want to change the defaults headers names (and it's a quite good idea).\n\nLet's start by navigating to the `Apikeys` plugin in the Designer of our route.\n\nThe first values to change are the headers names used to read the api key from client. Start by clicking on `extractors > CustomHeaders` and set the following values :\n\n* `api-key-header-id` as `Custom client id header name`\n* `api-key-header-secret` as `Custom client secret header name`\n\nSave the route, and call the service again.\n\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output an error because Otoroshi are expecting the api keys in other headers.\n\n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nCall one again the service but with the changed headers names.\n\n```sh\ncurl -X GET \\\n -H 'api-key-header-id: my-first-api-key-id' \\\n -H 'api-key-header-secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nAll others default services will continue to accept the api keys with the `Otoroshi-Client-Id` and `Otoroshi-Client-Secret` headers, whereas our service, will accept the `api-key-header-id` and `api-key-header-secret` headers.\n\n### Accept only api keys with expected values\n\nBy default, a secure service only accepts requests with api key. But all generated api keys are eligible to call our service and in some case, we want authorize only a couple of api keys.\n\nYou can restrict the list of accepted api keys by giving a list of `metadata` or/and `tags`. Each api key has a list of `tags` and `metadata`, which can be used by Otoroshi to validate a request with an api key. All api key metadata/tags can be forward to your service (see `Otoroshi Challenge` section of a service to get more information about `Otoroshi info. token`).\n\nLet's starting by only accepting api keys with the `otoroshi` tag.\n\nClick on the `ApiKeys` plugin, and enabled the `Routing` section. These constraints guarantee that a request will only be transmitted if all the constraints are validated.\n\nIn our first case, set `otoroshi` in `One Tag in` array and save the service.\nThen call our service with :\n```sh\ncurl -X GET \\\n -H 'Otoroshi-Client-Id: my-first-api-key-id' \\\n -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \\\n 'http://myservice.oto.tools:8080/api/test' --include\n```\n\nThis should output :\n```json\n// Error reason : Our api key doesn't contains the expected tag.\n{\n \"Otoroshi-Error\": \"Bad API key\"\n}\n```\n\nNavigate to the edit page of our api key, and jump to the `Metadata and tags` section.\nIn this section, add `otoroshi` in `Tags` array, then save the api key. Call once again your call and you will normally get a successful response of our backend service.\n\nIn this example, we have limited our service to API keys that have `otoroshi` as a tag.\n\nOtoroshi provides a few others behaviours. For each behaviour, *Api key used should*:\n\n* `All Tags in` : have all of the following tags\n* `No Tags in` : not have one of the following tags\n* `One Tag in` : have at least one of the following tags\n\n---\n\n* `All Meta. in` : have all of the following metadata entries\n* `No Meta. in` : not have one of the following metadata entries\n* `One Meta. in` : have at least one of the following metadata entries\n \n----\n\n* `One Meta key in` : have at least one of the following key in metadata\n* `All Meta key in` : have all of the following keys in metadata\n* `No Meta key in` : not have one of the following keys in metadata"},{"name":"secure-with-oauth1-client.md","id":"/how-to-s/secure-with-oauth1-client.md","url":"/how-to-s/secure-with-oauth1-client.html","title":"Secure an app with OAuth1 client flow","content":"# Secure an app with OAuth1 client flow\n\n
      \nRoute plugins:\nAuthentication\n
      \n\n### Before you start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Running an simple OAuth 1 server\n\nIn this tutorial, we'll instantiate a oauth 1 server with docker. If you alredy have the necessary, skip this section @ref:[to](#create-an-oauth-1-provider-module).\n\nLet's start by running the server\n\n```sh\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://otoroshi.oto.tools:8080/backoffice/auth0/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nWe created a oauth 1 server which accepts `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Redirect URI`. This URL is used by Otoroshi to retrieve a token and a profile at the end of an authentication process.\n\nAfter this command, the container logs should output :\n```sh \n127.0.0.1 - - [14/Oct/2021 12:10:49] \"HEAD /api/health HTTP/1.1\" 200 -\n```\n\n### Create an OAuth 1 provider module\n\n1. Go ahead, and navigate to @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new }\n1. Click on the cog icon on the top right\n1. Then **Authentication configs** button\n1. And add a new configuration when clicking on the **Add item** button\n2. Select the `Oauth1 provider` in the type selector field\n3. Set a basic name and description like `oauth1-provider`\n4. Set `2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET` as `Consumer key`\n5. Set `wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp` as `Consumer secret`\n6. Set `http://localhost:5000/oauth/request_token` as `Request Token URL`\n7. Set `http://localhost:5000/oauth/authorize` as `Authorize URL`\n8. Set `http://localhost:oauth/access_token` as `Access token URL`\n9. Set `http://localhost:5000/api/me` as `Profile URL`\n10. Set `http://otoroshi.oto.tools:8080/backoffice/auth0/callback` as `Callback URL`\n11. At the bottom of the page, disable the **secure** button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n At this point, your configuration should be similar to :\n\n\n\n\nWith this configuration, the connected user will receive default access on teams and organizations. If you want to change the access rights for a specific user, you can achieve it with the `Rights override` field and a configuration like :\n\n```json\n{\n \"foo@example.com\": [\n {\n \"tenant\": \"*:rw\",\n \"teams\": [\n \"*:rw\"\n ]\n }\n ]\n}\n```\n\nSave your configuration at the bottom of the page, then navigate to the `danger zone` to use your module as a third-party connection to the Otoroshi UI.\n\n### Connect to Otoroshi with OAuth1 authentication\n\nTo secure Otoroshi with your OAuth1 configuration, we have to register an Authentication configuration as a BackOffice Auth. configuration.\n\n1. Navigate to the **danger zone** (when clicking on the cog on the top right and selecting Danger zone)\n1. Scroll to the **BackOffice auth. settings**\n1. Select your last Authentication configuration (created in the previous section)\n1. Save the global configuration with the button on the top right\n\n### Testing your configuration\n\n1. Disconnect from your instance\n1. Then click on the **Login using third-party** button (or navigate to http://otoroshi.oto.tools:8080)\n2. Click on **Login using Third-party** button\n3. If all is configured, Otoroshi will redirect you to the oauth 1 server login page\n4. Set `example-user` as user and trust the user by clicking on `yes` button.\n5. Good work! You're connected to Otoroshi with an OAuth1 module.\n\n> A fallback solution is always available in the event of a bad authentication configuration. By going to http://otoroshi.oto.tools:8080/bo/simple/login, the administrators will be able to redefine the configuration.\n\n### Secure an app with OAuth 1 authentication\n\nWith the previous configuration, you can secure any of Otoroshi services with it. \n\nThe first step is to apply a little change on the previous configuration. \n\n1. Navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs](http://otoroshi.oto.tools:8080/bo/dashboard/auth-configs) { open=new }.\n2. Create a new auth module configuration with the same values.\n3. Replace the `Callback URL` field to `http://privateapps.oto.tools:8080/privateapps/generic/callback` (we changed this value because the redirection of a logged user by a third-party server is cover by an other route by Otoroshi).\n4. Disable the `secure` button (because we're using http and this configuration avoid to include cookie in an HTTP Request without secure channel, typically HTTPs)\n\n> Note : an Otoroshi service is called a private app when it is protected by an authentication module.\n\nOur example server supports only one redirect URI. We need to kill it, and to create a new container with `http://otoroshi.oto.tools:8080/privateapps/generic/callback` as `OAUTH1_REDIRECT_URI`\n\n```sh\ndocker rm -f oauth1-server\ndocker run -d --name oauth1-server --rm \\\n -p 5000:5000 \\\n -e OAUTH1_CLIENT_ID=2NVVBip7I5kfl0TwVmGzTphhC98kmXScpZaoz7ET \\\n -e OAUTH1_CLIENT_SECRET=wXzb8tGqXNbBQ5juA0ZKuFAmSW7RwOw8uSbdE3MvbrI8wjcbGp \\\n -e OAUTH1_REDIRECT_URI=http://privateapps.oto.tools:8080/privateapps/generic/callback \\\n ghcr.io/beryju/oauth1-test-server\n```\n\nOnce the authentication module and the new container created, we can define the authentication module on the service.\n\n1. Navigate to any created route\n2. Search in the list of plugins the plugin named `Authentication`\n3. Select your Authentication config inside the list\n4. Don't forget to save your configuration.\n\nNow you can try to call your route and see the login module appears.\n\n> \n\nThe allow access to the user.\n\n> \n\nIf you had any errors, make sure of :\n\n* check if you are on http or https, and if the **secure cookie option** is enabled or not on the authentication module\n* check if your OAuth1 server has the REDIRECT_URI set on **privateapps/...**\n* Make sure your server supports POST or GET OAuth1 flow set on authentication module\n\nOnce the configuration is working, you can check, when connecting with an Otoroshi admin user, the `Private App session` created (use the cog at the top right of the page, and select `Priv. app sesssions`, or navigate to @link:[http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private](http://otoroshi.oto.tools:8080/bo/dashboard/sessions/private) { open=new }).\n\nOne interesing feature is to check the profile of the connected user. In our case, when clicking on the `Profile` button of the right user, we should have : \n\n```json\n{\n \"email\": \"foo@example.com\",\n \"id\": 1,\n \"name\": \"test name\",\n \"screen_name\": \"example-user\"\n}\n```"},{"name":"secure-with-oauth2-client-credentials.md","id":"/how-to-s/secure-with-oauth2-client-credentials.md","url":"/how-to-s/secure-with-oauth2-client-credentials.html","title":"Secure an app with OAuth2 client_credential flow","content":"# Secure an app with OAuth2 client_credential flow\n\n\n\nOtoroshi makes it easy for your app to implement the [OAuth2 Client Credentials Flow](https://auth0.com/docs/authorization/flows/client-credentials-flow). \n\nWith machine-to-machine (M2M) applications, the system authenticates and authorizes the app rather than a user. With the client credential flow, applications will pass along their Client ID and Client Secret to authenticate themselves and get a token.\n\n## Deployed the Client Credential Service\n\nThe Client Credential Service must be enabled as a global plugin on your Otoroshi instance. Once enabled, it will expose three endpoints to issue and validate tokens for your routes.\n\nLet's navigate to your otoroshi instance (in our case http://otoroshi.oto.tools:8080) on the danger zone (`top right cog icon / Danger zone` or at [/bo/dashboard/dangerzone](http://otoroshi.oto.tools:8080/bo/dashboard/dangerzone)).\n\nTo enable a plugin in global on Otoroshi, you must add it in the `Global Plugins` section.\n\n1. Open the `Global Plugin` section \n2. Click on `enabled` (if not already done)\n3. Search the plugin named `Client Credential Service` of type `Sink` (you need to enabled it on the old or new Otoroshi engine, depending on your use case)\n4. Inject the default configuration by clicking on the button (if you are using the old Otoroshi engine)\n\nIf you click on the arrow near each plugin, you will have the documentation of the plugin and its default configuration.\n\nThe client credential plugin has by default 4 parameters : \n\n* `domain`: a regex used to expose the three endpoints (`default`: *)\n* `expiration`: duration until the token expire (in ms) (`default`: 3600000)\n* `defaultKeyPair`: a key pair used to sign the jwt token. By default, Otoroshi is deployed with an otoroshi-jwt-signing that you can visualize on the jwt verifiers certificates (`default`: \"otoroshi-jwt-signing\")\n* `secure`: if enabled, Otoroshi will expose routes only in the https requests case (`default`: true)\n\nIn this tutorial, we will set the configuration as following : \n\n* `domain`: oauth.oto.tools\n* `expiration`: 3600000\n* `defaultKeyPair`: otoroshi-jwt-signing\n* `secure`: false\n\nNow that the plugin is running, third routes are exposed on each matching domain of the regex.\n\n* `GET /.well-known/otoroshi/oauth/jwks.json` : retrieve all public keys presents in Otoroshi\n* `POST /.well-known/otoroshi/oauth/token/introspect` : validate and decode the token \n* `POST /.well-known/otoroshi/oauth/token` : generate a token with the fields provided\n\nOnce the global configuration saved, we can deployed a simple service to test it.\n\nLet's navigate to the routes page, and create a new route with : \n\n1. `foo.oto.tools` as `domain` in the frontend node\n2. `mirror.otoroshi.io` as hostname in the list of targets of the backend node, and `443` as `port`.\n3. Search in the list of plugins and add the `Apikeys` plugin to the flow\n4. In the extractors section of the `Apikeys` plugin, disabled the `Basic`, `Client id` and `Custom headers` option.\n5. Save your route\n\nLet's make a first call, to check if the jwks are already exposed :\n\n```sh\ncurl 'http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json'\n```\n\nThe output should look like a list of public keys : \n```sh\n{\n \"keys\": [\n {\n \"kty\": \"RSA\",\n \"e\": \"AQAB\",\n \"kid\": \"otoroshi-intermediate-ca\",\n ...\n }\n ...\n ]\n}\n``` \n\nLet's make a call to your route. \n\n```sh\ncurl 'http://foo.oto.tools:8080/'\n```\n\nThis should output the expected error: \n```json\n{\n \"Otoroshi-Error\": \"No ApiKey provided\"\n}\n```\n\nThe first step is to generate an api key. Navigate to the api keys page, and create an item with the following values (it will be more easy to use them in the next step)\n\n* `my-id` as `ApiKey Id`\n* `my-secret` as `ApiKey Secret`\n\nThe next step is to get a token by calling the endpoint `http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/jwks.json`. The required fields are the grand type, the client and the client secret corresponding to our generated api key.\n\n```sh\ncurl -X POST http://oauth.oto.tools:8080/.well-known/otoroshi/oauth/token \\\n-H \"Content-Type: application/json\" \\\n-d @- <<'EOF'\n{\n \"grant_type\": \"client_credentials\",\n \"client_id\":\"my-id\",\n \"client_secret\":\"my-secret\"\n}\nEOF\n```\n\nThis request have one more optional field, named `scope`. The scope can be used to set a bunch of scope on the generated access token.\n\nThe last command should look like : \n\n```sh\n{\n \"access_token\": \"generated-token-xxxxx\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600\n}\n```\n\nNow we can call our api with the generated token\n\n```sh\ncurl 'http://foo.oto.tools:8080/' \\\n -H \"Authorization: Bearer generated-token-xxxxx\"\n```\n\nThis should output a successful call with the list of headers with a field named `Authorization` containing the previous access token.\n\n## Other possible configuration\n\nBy default, Otoroshi generate the access token with the specified key pair in the configuration. But, in some case, you want a specific key pair by client_id/client_secret.\nThe `jwt-sign-keypair` metadata can be set on any api key with the id of the key pair as value. \n"},{"name":"setup-otoroshi-cluster.md","id":"/how-to-s/setup-otoroshi-cluster.md","url":"/how-to-s/setup-otoroshi-cluster.html","title":"Setup an Otoroshi cluster","content":"# Setup an Otoroshi cluster\n\n
      \nRoute plugins:\nAdditional Headers In\n
      \n\nIn this tutorial, you create an cluster of Otoroshi.\n\n### Summary \n\n1. Deploy an Otoroshi cluster with one leader and 2 workers \n2. Add a load balancer in front of the workers \n3. Validate the installation by adding a header on the requests\n\nLet's start by downloading the latest jar of Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nThen create an instance of Otoroshi and indicates with the `otoroshi.cluster.mode` environment variable that it will be the leader.\n\n```sh\njava -Dhttp.port=8091 -Dhttps.port=9091 -Dotoroshi.cluster.mode=leader -jar otoroshi.jar\n```\n\nLet's create two Otoroshi workers, exposed on `:8082/:8092` and `:8083/:8093` ports, and setting the leader URL in the `otoroshi.cluster.leader.urls` environment variable.\n\nThe first worker will listen on the `:8082/:8092` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-1 \\\n -Dhttp.port=8092 \\\n -Dhttps.port=9092 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nThe second worker will listen on the `:8083/:8093` ports\n```sh\njava \\\n -Dotoroshi.cluster.worker.name=worker-2 \\\n -Dhttp.port=8093 \\\n -Dhttps.port=9093 \\\n -Dotoroshi.cluster.mode=worker \\\n -Dotoroshi.cluster.leader.urls.0='http://127.0.0.1:8091' -jar otoroshi.jar\n```\n\nOnce launched, you can navigate to the @link:[cluster view](http://otoroshi.oto.tools:8091/bo/dashboard/cluster) { open=new }. The cluster is now configured, you can see the 3 instances and some health informations on each instance.\n\nTo complete our installation, we want to spread the incoming requests accross otoroshi worker instances. \n\nIn this tutorial, we will use `haproxy` has a TCP loadbalancer. If you don't have haproxy installed, you can use docker to run an haproxy instance as explained below.\n\nBut first, we need an haproxy configuration file named `haproxy.cfg` with the following content :\n\n```sh\nfrontend front_nodes_http\n bind *:8080\n mode tcp\n default_backend back_http_nodes\n timeout client 1m\n\nbackend back_http_nodes\n mode tcp\n balance roundrobin\n server node1 host.docker.internal:8092 # (1)\n server node2 host.docker.internal:8093 # (1)\n timeout connect 10s\n timeout server 1m\n```\n\nand run haproxy with this config file\n\nno docker\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #no_docker }\n\ndocker (on linux)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_linux }\n\ndocker (on macos)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_mac }\n\ndocker (on windows)\n: @@snip [run.sh](../snippets/cluster-run-ha.sh) { #docker_windows }\n\nThe last step is to create a route, add a rule to add, in the headers, a specific value to identify the worker used.\n\nCreate this route, exposed on `http://api.oto.tools:xxxx`, which will forward all requests to the mirror `https://mirror.otoroshi.io`.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8091/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"myapi\",\n \"frontend\": {\n \"domains\": [\"api.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"worker-name\": \"${config.otoroshi.cluster.worker.name}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nOnce created, call two times the service. If all is working, the header received by the backend service will have `worker-1` and `worker-2` as value.\n\n```sh\ncurl 'http://api.oto.tools:8080'\n## Response headers\n{\n ...\n \"worker-name\": \"worker-2\"\n ...\n}\n```\n\nThis should output `worker-1`, then `worker-2`, etc. Well done, your loadbalancing is working and your cluster is set up correctly.\n\n\n"},{"name":"tailscale-integration.md","id":"/how-to-s/tailscale-integration.md","url":"/how-to-s/tailscale-integration.html","title":"Tailscale integration","content":"# Tailscale integration\n\n[Tailscale](https://tailscale.com/) is a VPN service that let you create your own private network based on [Wireguard](https://www.wireguard.com/). Tailscale goes beyond the simple meshed wireguard based VPN and offers out of the box NAT traversal, third party identity provider integration, access control, magic DNS and let's encrypt integration for the machines on your VPN.\n\nOtoroshi provides somes plugins out of the box to work in a [Tailscale](https://tailscale.com/) environment.\n\nby default Otoroshi, works out of the box when integrated in a `tailnet` as you can contact other machines usign their ip address. But we can go a little bit further.\n\n## tailnet configuration\n\nfirst thing, go to your tailnet setting on [tailscale.com](https://login.tailscale.com/admin/machines) and go to the [DNS tab](https://login.tailscale.com/admin/dns). Here you can find \n\n* your tailnet name: the domain name of all your machines on your tailnet\n* MagicDNS: a way to address your machines by directly using their names\n* HTTPS Certificates: HTTPS certificates provision for all your machines\n\nto use otoroshi Tailscale plugin you must enable `MagicDNS` and `HTTPS Certificates`\n\n## Tailscale certificates integration\n\nyou can use tailscale generated let's encrypt certificates in otoroshi by using the `Tailscale certificate fetcher job` in the plugins section of the danger zone. Once enabled, this job will fetch certificates for domains in `xxxx.ts.net` that belong to your tailnet. \n\nas usual, the fetched certificates will be available in the [certificates page](http://otoroshi.oto.tools:8080/bo/dashboard/certificates) of otoroshi.\n\n## Tailscale targets integration\n\nthe following pair of plugins let your contact tailscale machine by using their names even if their are multiple instance.\n\nwhen you register a machine on a tailnet, you have to provide a name for it, let say `my-server`. This machine will be addressable in your tailnet with `my-server.tailxxx.ts.net`. But if you have multiple instance of the same server on several machines with the same `my-server` name, their DNS name on the tailnet will be `my-server.tailxxx.ts.net`, `my-server-1.tailxxx.ts.net`, `my-server-2.tailxxx.ts.net`, etc. If you want to use those names in an otoroshi backend it could be tricky if the application has something like autoscaling enabled.\n\nin that case, you can add the `Tailscale targets job` in the plugins section of the danger zone. Once enabled, this job will fetch periodically available machine on the tailnet with their names and DNS names. Then, in a route, you can use the `Tailscale select target by name` plugin to tell otoroshi to loadbalance traffic between all machine that have the name specified in the plugin config. instead of their DNS name."},{"name":"tls-termination-using-own-certificates.md","id":"/how-to-s/tls-termination-using-own-certificates.md","url":"/how-to-s/tls-termination-using-own-certificates.html","title":"TLS termination using your own certificates","content":"# TLS termination using your own certificates\n\nThe goal of this tutorial is to expose a service via https using a certificate generated by openssl.\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\nTry to call the service.\n\n```sh\ncurl 'http://myservice.oto.tools:8080'\n```\n\nThis should output something like\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463145856319359618\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8080\",\n \"opun-gateway-request-id\": \"1463145856554240100\",\n \"x-forwarded-proto\": \"https\",\n },\n \"body\": \"\"\n}\n```\n\nLet's try to call the service in https.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nThis should output\n\n```sh\ncurl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to myservice.oto.tools:8443\n```\n\nTo fix it, we have to generate a certificate and import it in Otoroshi to match the domain `myservice.oto.tools`.\n\n> If you already had a certificate you can skip the next set of commands and directly import your certificate in Otoroshi\n\nWe will use openssl to generate a private key and a self-signed certificate.\n\n```sh\nopenssl genrsa -out myservice.key 4096\n# remove pass phrase\nopenssl rsa -in myservice.key -out myservice.key\n# generate the certificate authority cert\nopenssl req -new -x509 -sha256 -days 730 -key myservice.key -out myservice.cer -subj \"/CN=myservice.oto.tools\"\n```\n\nCheck the content of the certificate \n\n```sh\nopenssl x509 -in myservice.cer -text\n```\n\nThis should contains something like\n\n```sh\nCertificate:\n Data:\n Version: 1 (0x0)\n Serial Number: 9572962808320067790 (0x84d9fef455f188ce)\n Signature Algorithm: sha256WithRSAEncryption\n Issuer: CN=myservice.oto.tools\n Validity\n Not Before: Nov 23 14:25:55 2021 GMT\n Not After : Nov 23 14:25:55 2022 GMT\n Subject: CN=myservice.oto.tools\n Subject Public Key Info:\n Public Key Algorithm: rsaEncryption\n Public-Key: (4096 bit)\n Modulus:\n...\n```\n\nOnce generated, go back to Otoroshi and navigate to the certificates management page (`top right cog icon / SSL/TLS certificates` or at @link:[`/bo/dashboard/certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates)) and click on `Add item`.\n\nSet `myservice-certificate` as `name` and `description`.\n\nDrop the `myservice.cer` file or copy the content to the `Certificate full chain` field.\n\nDo the same action for the `myservice.key` file in the `Certificate private key` field.\n\nSet your passphrase password in the `private key password` field if you added one.\n\nLet's try the same call to the service.\n\n```sh\ncurl 'https://myservice.oto.tools:8443'\n```\n\nAn error should occurs due to the untrsuted received certificate server\n\n```sh\ncurl: (60) SSL certificate problem: self signed certificate\nMore details here: https://curl.haxx.se/docs/sslcerts.html\n\ncurl failed to verify the legitimacy of the server and therefore could not\nestablish a secure connection to it. To learn more about this situation and\nhow to fix it, please visit the web page mentioned above.\n```\n\nEnd this tutorial by trusting the certificate server \n\n```sh\ncurl 'https://myservice.oto.tools:8443' --cacert myservice.cer\n```\n\nThis should finally output\n\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/\",\n \"headers\": {\n \"host\": \"mirror.opunmaif.io\",\n \"accept\": \"*/*\",\n \"user-agent\": \"curl/7.64.1\",\n \"x-forwarded-port\": \"443\",\n \"opun-proxied-host\": \"mirror.otoroshi.io\",\n \"otoroshi-request-id\": \"1463158439730479893\",\n \"otoroshi-proxied-host\": \"myservice.oto.tools:8443\",\n \"opun-gateway-request-id\": \"1463158439558515871\",\n \"x-forwarded-proto\": \"https\",\n \"sozu-id\": \"01FN6MGKSYZNJYHEMP4R5PJ4Q5\"\n },\n \"body\": \"\"\n}\n```\n\n"},{"name":"tls-using-lets-encrypt.md","id":"/how-to-s/tls-using-lets-encrypt.md","url":"/how-to-s/tls-using-lets-encrypt.html","title":"TLS termination using Let's Encrypt","content":"# TLS termination using Let's Encrypt\n\nAs you know, Otoroshi is capable of doing TLS termination for your services. You can import your own certificates, generate certificates from scratch and you can also use the @link:[ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555) to generate certificates. One of the most popular service offering ACME certificates creation is @link:[Let's Encrypt](https://letsencrypt.org/).\n\n@@@ warning\nIn order to make this tutorial work, your otoroshi instance MUST be accessible from the internet in order to be reachable by Let's Encrypt ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n@@@ note\nthis tutorial can work with any ACME provider with the same rules. your otoroshi instance MUST be accessible by the ACME process. Also, the domain name used for the certificates MUST be configured to reach your otoroshi instance at your DNS provider level.\n@@@\n\n## Setup let's encrypt on otoroshi\n\nGo on the danger zone page by clicking on the [`cog icon / Danger Zone`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Scroll to the `Let's Encrypt settings` section. Enable it, and specify the address of the ACME server (for production Let's Encrypt it's `acme://letsencrypt.org`, for testing, it's `acme://letsencrypt.org/staging`. Any ACME server address should work). You can also add one or more email addresses or contact urls that will be included in your Let's Encrypt account. You don't have to fill the `public/private key` inputs as they will be automatically generated on the first usage.\n\n## Creating let's encrypt certificate from FQDNs\n\nYou can go to the certificates page by clicking on the [`cog icon / SSL/TLS Certificates`](http://otoroshi.oto.tools:8080/bo/dashboard/certificates). Here, click on the `+ Let's Encrypt certificate` button. A popup will show up to ask you the FQDN that you want for you certificate. Once done, click on the `Create` button. A few moment later, you will be redirected on a brand new certificate generated by Let's encrypt. You can now enjoy accessing your service behind the FQDN with TLS.\n\n## Creating let's encrypt certificate from a service\n\nYou can go to any service page and enable the flag `Issue Let's Encrypt cert.`. Do not forget to save your service. A few moment later, the certificates will be available in the certificates page and you can will be able to enjoy accessing your service with TLS.\n"},{"name":"wasm-usage.md","id":"/how-to-s/wasm-usage.md","url":"/how-to-s/wasm-usage.html","title":"Using wasm plugins","content":"# Using wasm plugins\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. You can find more about those plugins @ref:[here](../topics/wasm-usage.md)\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create the route with the plugin validator](#create-the-route-with-the-plugin-validator)\n3. [Test your validator](#test-your-validator)\n4. [Update the route by replacing the backend with a WASM file](#update-the-route-by-replacing-the-backend-with-a-wasm-file)\n5. [WASM backend test](#wasm-backend-test)\n6. [Expose a single file as WASM backend](#expose-a-single-file-as-wasm-backend)\n\nAfter completing these steps you will have a route that uses WASM plugins written in Rust.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create the route with the plugin validator\n\nFor this tutorial, we will start with an existing wasm file. The main function of this file will check the value of an http header to allow access or not. The can find this file at [https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm)\n\nThe main function of this validator, written in rust, should look like:\n\nvalidator.rs\n: @@snip [validator.rs](../snippets/wasmo/validator.rs) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.ts\n: @@snip [validator.ts](../snippets/wasmo/validator.ts) \n\nvalidator.js\n: @@snip [validator.js](../snippets/wasmo/validator.js) \n\nvalidator.go\n: @@snip [validator.js](../snippets/wasmo/validator.go) \n\nThe plugin receives the request context from Otoroshi (the matching route, the api key if present, the headers, etc) as `WasmAccessValidatorContext` object. \nThen it applies a check on the headers, and responds with an error or success depending on the content of the foo header. \nObviously, the previous snippet is an example and the editor allows you to write whatever you want as a check.\n\nLet's create a route that uses the previous wasm file as an access validator plugin :\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThis request will apply the following process:\n\n* names the route *demo-otoroshi*\n* creates a frontend exposed on the `demo-otoroshi.oto.tools` \n* forward requests on one target, reachable at `mirror.otoroshi.io` using TLS on port 443\n* adds the *WasmAccessValidator* plugin to validate access based on the foo header to the route\n\nYou can validate the route creation by navigating to the [dashboard](http://otoroshi.oto.tools:8080/bo/dashboard/routes/demo-otoroshi?tab=flow)\n\n## Test your validator\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -I\n```\n\nThis should output the following error:\n\n```\nHTTP/1.1 401 Unauthorized\n```\n\nLet's call again the route by adding the header foo with the bar value.\n\n```shell\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -I\n```\n\nThis should output the successfull message:\n\n```\nHTTP/1.1 200 OK\n```\n\n## Update the route by replacing the backend with a WASM file\n\nThe next step in this tutorial is to use a WASM file as backend of the route. We will use an existing WASM file, available in our wasm demos repository on github. \nThe content of this plugin, called `wasm-target.wasm`, looks like:\n\ntarget.rs\n: @@snip [target.rs](../snippets/wasmo/target.rs) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.ts\n: @@snip [target.ts](../snippets/wasmo/target.ts) \n\ntarget.js\n: @@snip [target.js](../snippets/wasmo/target.js) \n\ntarget.go\n: @@snip [target.js](../snippets/wasmo/target.go) \n\nLet's explain this snippet. The purpose of this type of plugin is to respond an HTTP response with http status, body and headers map.\n\n1. Includes all public structures from `types.rs` file. This file contains predefined Otoroshi structures that plugins can manipulate.\n2. Necessary imports. [Extism](https://extism.org/docs/overview)'s goal is to make all software programmable by providing a plug-in system. \n3. Creates a map of new headers that will be merged with incoming request headers.\n4. Creates the response object with the map of merged headers, a simple JSON body and a successfull status code.\n\nThe file is downloadable [here](#https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm).\n\nLet's update the route using the this wasm file.\n\n```sh\ncurl -X PUT \"http://otoroshi-api.oto.tools:8080/api/routes/demo-otoroshi\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"demo-otoroshi\",\n \"name\": \"demo-otoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n \"enabled\": true\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmBackend\",\n \"enabled\": true,\n \"config\": {\n \"source\": {\n \"kind\": \"http\",\n \"path\": \"https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm\",\n \"opts\": {}\n },\n \"memoryPages\": 4,\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nThe response should contains the updated route content.\n\n## WASM backend test\n\nLet's call our route.\n\n```sh\ncurl \"http://demo-otoroshi.oto.tools:8080\" -H \"foo:bar\" -H \"fifi: foo\" -v\n```\n\nThis should output:\n\n```\n* Trying 127.0.0.1:8080...\n* Connected to demo-otoroshi.oto.tools (127.0.0.1) port 8080 (#0)\n> GET / HTTP/1.1\n> Host: demo-otoroshi.oto.tools:8080\n> User-Agent: curl/7.79.1\n> Accept: */*\n> foo:bar\n> fifi:foo\n>\n* Mark bundle as not supporting multiuse\n< HTTP/1.1 200 OK\n< foo: bar\n< Host: demo-otoroshi.oto.tools:8080\n<\n* Closing connection 0\n{\"foo\": \"bar\"}\n```\n\nIn this response, we can find our headers send in the curl command and those added by the wasm plugin.\n\n\n## Expose a single file as WASM backend\n\nA WASM backend plugin can directly expose a file written in Wasmo. This is the simplest possibility to write HTML, Javascript, JSON or expose a simple PNG image.\n\nLet's expose a HTML page. In your Wasmo instance, execute the following instructions:\n\n1. Click the new plugin button\n2. Add a name and `validate`\n3. Click the new plugin\n4. Create a new file named `index.html`\n5. Copy and paste the following content\n\n```html\n \n\n\n Wasmo plugin\n\n\n

      Hello from Wasmo

      \n\n\n```\n\nThis snippet is a short HTML template with a title to indicate that it comes from Wasmo. \n\nNow we can write our javascript function to parse and return the content of our HTML to Otoroshi. \n\n1. Navigate to the `index.js` file\n2. Replace the content with the following content\n\n```js\nimport IndexPage from './index.html'\n\nexport function execute() {\n \n let response = {\n headers: {\n 'Content-Type': 'text/html; charset=utf-8'\n },\n body: IndexPage,\n status: 200\n };\n \n Host.outputString(JSON.stringify(response));\n\n return 0;\n}\n```\n\nThe code is pretty self-explanatory. We start by importing our HTML page and we build the response with the correct content type, the body and a 200 http status.\n\nJust before testing, we need to change the esbuild configuration to specify how to bundle the HTML file.\n\nThe contents of your `esbuild.js` file should look this:\n\n```js\nconst esbuild = require('esbuild');\n\nesbuild\n .build({\n entryPoints: ['index.js'],\n outdir: 'dist',\n bundle: true,\n loader: {\n '.html': 'text'\n },\n sourcemap: true,\n minify: false, // might want to use true for production build\n format: 'cjs', // needs to be CJS for now\n target: ['es2020'] // don't go over es2020 because quickjs doesn't support it\n })\n```\n\nCheck your browser at `http://demo-otoroshi.oto.tools:8080` and you should see your page content updated to the new text.\n\nIf you need to expose more than a HTML page, we highly recommend to use the @ref:[Zip Backend plugin](../how-to-s/zip-backend-plugin.md)"},{"name":"wasmo-installation.md","id":"/how-to-s/wasmo-installation.md","url":"/how-to-s/wasmo-installation.html","title":"Deploy your own Wasmo","content":"# Deploy your own Wasmo\n\nInstalling Wasmo can be done by following the [Getting Started](https://maif.github.io/wasmo/builder/getting-started) in Wasmo documentation.\n\n## Tutorial\n\n- [Deploy your own Wasmo](#deploy-your-own-wasmo)\n - [Tutorial](#tutorial)\n - [Before your start](#before-your-start)\n - [Create a route to expose and protect Wasmo with authentication](#create-a-route-to-expose-and-protect-wasmo-with-authentication)\n - [Create a first validator plugin using Wasmo](#create-a-first-validator-plugin-using-wasmo)\n - [Pairing Otoroshi and Wasmo](#pairing-otoroshi-with-wasmo)\n - [Create a route using the generated wasm file](#create-a-route-using-the-generated-wasm-file)\n - [Test your route](#test-your-route)\n\nAfter completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.\n\n### Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create a route to expose and protect Wasmo with authentication\n\nWe are going to use the admin API of Otoroshi to create the route. The configuration of the route is:\n\n* `wasmo` as name\n* `wasmo.oto.tools` as exposed domain\n* `localhost:5001` as target without TLS option enabled\n\nWe need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. \nThese plugins are named `Authentication` and `Otoroshi Info. token`. \nThe Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). \nThe second plugin will be configured with the value of the `OTOROSHI_USER_HEADER` environment variable. \n\nLet's create the authentication module (if you are interested in how authentication module works, \nyou should read the other tutorials about How to secure an app). \nThe following command creates an in-memory authentication module with an user.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/auths\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo_in_memory\",\n \"type\": \"basic\",\n \"name\": \"In memory authentication\",\n \"desc\": \"Group of static users\",\n \"users\": [\n {\n \"name\": \"User Otoroshi\",\n \"password\": \"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\n \"email\": \"wasm@otoroshi.io\"\n }\n ],\n \"sessionCookieValues\": {\n \"httpOnly\": true,\n \"secure\": false\n }\n}\nEOF\n```\n\nOnce created, you can create our route to expose Wasmo.\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u \"admin-api-apikey-id:admin-api-apikey-secret\" \\\n-d @- <<'EOF'\n{\n \"id\": \"wasmo\",\n \"name\": \"wasmo\",\n \"frontend\": {\n \"domains\": [\"wasmo.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"exclude\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {\n \"pass_with_apikey\": false,\n \"auth_module\": null,\n \"module\": \"wasmo_in_memory\"\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\": [\n \"/plugins\",\n \"/wasm/.*\"\n ],\n \"config\": {}\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OtoroshiInfos\",\n \"config\": {\n \"version\": \"Latest\",\n \"ttl\": 30,\n \"header_name\": \"Otoroshi-User\",\n \"algo\": {\n \"type\": \"HSAlgoSettings\",\n \"size\": 512,\n \"secret\": \"veryverysecret\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nTry to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. \nThis should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password\nCongratulations, you now have secured Wasmo.\n\n### Create a first validator plugin using Wasmo\n\nIn the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. \nThis plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.\n\n1. On the right top of the screen, click on the plus icon to create a new plugin\n2. Select the Rust language\n3. Call it `my-first-validator` and press the enter key\n4. Click on the new plugin called `my-first-validator`\n\nBefore continuing, let's explain the different files already present in your plugin. \n\n* `types.rs`: this file contains all Otoroshi structures that the plugin can receive and respond\n* `lib.rs`: this file is the core of your plugin. It must contain at least one **function** which will be called by Otoroshi when executing the plugin.\n* `Cargo.toml`: for each rust package, this file is called its manifest. It is written in the TOML format. \nIt contains metadata that is needed to compile the package. You can read more information about it [here](https://doc.rust-lang.org/cargo/reference/manifest.html)\n\nYou can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. \nIn terms of plugin type,\nyou need to change your plugin's context and reponse types accordingly.\n\nLet's take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding \ntypes named: `WasmAccessValidatorContext` and `WasmAccessValidatorResponse`.\nThese types must be use in the declaration of the main **function** (named execute in our case).\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n \n}\n```\n\nWith this code, we declare a function named `execute`, which takes a context of type WasmAccessValidatorContext as parameter, \nand which returns an object of type WasmAccessValidatorResponse. Now, let's add the check of the foo header.\n\n```rust\n... \npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nFirst, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError.\nIn the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file:\n`WasmAccessValidatorResponse`, `WasmAccessValidatorError` and `WasmAccessValidatorContext`. \n\nAt this time, the content of your lib.rs file should be:\n\n```rust\nmod types;\n\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn execute(Json(context): Json) -> FnResult> {\n match context.request.headers.get(\"foo\") {\n Some(foo) => if foo == \"bar\" {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: true,\n error: None\n }))\n } else {\n Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: format!(\"{} is not authorized\", foo).to_owned(), \n status: 401\n }) \n }))\n },\n None => Ok(Json(types::WasmAccessValidatorResponse { \n result: false, \n error: Some(types::WasmAccessValidatorError { \n message: \"you're not authorized\".to_owned(), \n status: 401\n }) \n }))\n }\n}\n```\n\nLet's compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI.\nClick on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. \nOnce done, click on the run button at the bottom of your screen. This should output an error.\n\n```json\n{\n \"result\": false,\n \"error\": {\n \"message\": \"asd is not authorized\",\n \"status\": 401\n }\n}\n```\n\nLet's edit the fake input context by adding the exepected foo Header.\n\n```json\n{\n \"request\": {\n \"id\": 0,\n \"method\": \"\",\n \"headers\": {\n \"foo\": \"bar\"\n },\n \"cookies\"\n ...\n```\n\nResubmit the command. It should pass.\n\n### Pairing Otoroshi and Wasmo\n\nNow that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let's navigate to the danger zone, and add the following values in the Wasmo section:\n\n* `URL`: http://localhost:5001\n* `Apikey id`: admin-api-apikey-id\n* `Apikey secret`: admin-api-apikey-secret\n* `User(s)`: *\n* `Token secret`:\n\nThe User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user). \n\nDon't forget to save the configuration.\n\n### Create a route using the generated wasm file\n\nThe last step of our tutorial is to create the route using the validator. Let's create the route with the following parameters:\n\n```sh\ncurl -X POST \"http://otoroshi-api.oto.tools:8080/api/routes\" \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"id\": \"wasm-route\",\n \"name\": \"wasm-route\",\n \"frontend\": {\n \"domains\": [\"wasm-route.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"localhost\",\n \"port\": 5001,\n \"tls\": false\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.WasmAccessValidator\",\n \"enabled\": true,\n \"config\": {\n \"compiler_source\": \"my-first-validator\",\n \"functionName\": \"execute\"\n }\n }\n ]\n}\nEOF\n```\n\nYou can validate the creation by navigating to the [dashboard](http://otoroshi.oto.tools:9999/bo/dashboard/routes/wasm-route?tab=flow)\n\n### Test your route\n\nRun the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.\n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\"\n```\n\nand \n\n```sh\ncurl \"http://wasm-route.oto.tools:8080\" -H \"foo:bar\"\n```\n\nCongratulations, you have successfully written your first validator using your own Wasmo.\n"},{"name":"working-with-eureka.md","id":"/how-to-s/working-with-eureka.md","url":"/how-to-s/working-with-eureka.html","title":"Working with Eureka","content":"# Working with Eureka\n\n\n\nEureka is a library of Spring Cloud Netflix, which provides two parts to register and discover services.\nGenerally, the services are applications written with Spring but Eureka also provides a way to communicate in REST. The main goals of Eureka are to allow clients to find and communicate with each other without hard-coding the hostname and port.\nAll services are registered in an Eureka Server.\n\nTo work with Eureka, Otoroshi has three differents plugins:\n\n* to expose its own Eureka Server instance\n* to discover an existing Eureka Server instance\n* to use Eureka application as an Otoroshi target and took advantage of all Otoroshi clients features (load-balancing, rate limiting, etc...)\n\nLet's cut this tutorial in three parts. \n\n- Create an simple Spring application that we'll use as an Eureka Client\n- Deploy an implementation of the Otoroshi Eureka Server (using the `Eureka Instance` plugin), register eureka clients and expose them using the `Internal Eureka Server` plugin\n- Deploy an Netflix Eureka Server and use it in Otoroshi to discover apps using the `External Eureka Server` plugin.\n\n\nIn this tutorial: \n\n- [Create an Otoroshi route with the Internal Eureka Server plugin](#create-an-otoroshi-route-with-the-internal-eureka-server-plugin)\n- [Create a simple Eureka Client and register it](#create-a-simple-eureka-client-and-register-it)\n- [Connect to an external Eureka server](#connect-to-an-external-eureka-server)\n\n### Download Otoroshi\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n### Create an Otoroshi route with the Internal Eureka Server plugin\n\n@@@ note\nWe'll supposed that you have an Otoroshi exposed on the 8080 port with the new Otoroshi engine enabled\n@@@\n\nLet's jump to the routes Otoroshi [view](http://otoroshi.oto.tools:8080/bo/dashboard/routes) and create a new route using the wizard button.\n\nEnter the following values in for each step:\n\n1. An Eureka Server instance\n2. Choose the first choice : **BLANK ROUTE** and click on continue\n3. As exposed domain, set `eureka-server.oto.tools/eureka`\n4. As Target URL, set `http://foo.bar` (this value has no importance and will be skip by the Otoroshi Instance plugin)\n5. Validate the creation\n\nOnce created, you can hide with the arrow on the right top of the screen the tester view (which is displayed by default after each route creation).\nIn our case, we want to add a new plugin, called Internal Eureka Instance on our feed.\n\nInside the designer view:\n\n1. Search the `Eureka Instance` in the list of plugins.\n2. Add it to the feed by clicking on it\n3. Set an eviction timeout at 300 seconds (this configuration is used by Otoroshi to automatically check if an Eureka is up. Otherwise Otoroshi will evict the eureka client from the registry)\n\nWell done you have set up an Eureka Server. To check the content of an Eureka Server, you can navigate to this [link]('http://otoroshi.oto.tools:8080/bo/dashboard/eureka-servers'). In all case, none instances or applications are registered, so the registry is currently empty.\n\n### Create a simple Eureka Client and register it\n\n*This tutorial has no vocation to teach you how to write an Spring application and it may exists a newer version of this Spring code.*\n\n\nFor this tutorial, we'll use the following code which initiates an Eureka Client and defines an Spring REST Controller with only one endpoint. This endpoint will return its own exposed port (this value will be useful to check that the Otoroshi load balancing is right working between the multiples Eureka instances registered).\n\n\nLet's fast create a Spring project using [Spring Initializer](https://start.spring.io/). You can use the previous link or directly click on the following link to get the form already filled with the needed dependencies.\n\n````bash\nhttps://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.3&packaging=jar&jvmVersion=17&groupId=otoroshi.io&artifactId=eureka-client&name=eureka-client&description=A%20simple%20eureka%20client&packageName=otoroshi.io.eureka-client&dependencies=cloud-eureka,web\n````\n\nFeel free to change the project metadata for your use case.\n\nOnce downloaded and uncompressed, let's ahead and start to delete the application.properties and create an application.yml (if you are more comfortable with an application.properties, keep it)\n\n````yaml\neureka:\n client:\n fetch-registry: false # disable the discovery services mechanism for the client\n serviceUrl:\n defaultZone: http://eureka-server.oto.tools:8080/eureka\n\nspring:\n application:\n name: foo_app\n\n````\n\n\nNow, let's define the simple REST controller to expose the client port.\n\nCreate a new file, called PortController.java, in the sources folder of your project with the following content.\n\n````java\npackage otoroshi.io.eurekaclient;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.env.Environment;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class PortController {\n\n @Autowired\n Environment environment;\n\n @GetMapping(\"/port\")\n public String index() {\n return environment.getProperty(\"local.server.port\");\n }\n}\n````\nThis controller is very simple, we just exposed one endpoint `/port` which returns the port as string. Our client is ready to running. \n\nLet's launch it with the following command:\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085\n````\n\n@@@note\nThe port is not required but it will be useful when we will deploy more than one instances in the rest of the tutorial\n@@@\n\n\nOnce the command ran, you can navigate to the eureka server view in the Otoroshi UI. The dashboard should displays one registered app and instance.\nIt should also displays a timer for each application which represents the elapsed time since the last received heartbeat.\n\nLet's define a new route to exposed our registered eureka client.\n\n* Create a new route, named `Eureka client`, exposed on `http://eureka-client.oto.tools:8080` and targeting `http://foo.bar`\n* Search and add the `Internal Eureka server` plugin \n* Edit the plugin and choose your eureka server and your app (in our case, `Eureka Server` and `FOO_APP` respectively)\n* Save your route\n\nNow try to call the new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n````\n\nIf everything is working, you should get the port 8085 as the response.The setup is working as expected, but we can improve him by scaling our eureka client.\n\nOpen a new tab in your terminal and run the following command.\n\n````sh\nmvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8083\n````\n\nJust wait a few seconds and retry to call your new route.\n\n````sh\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8085\ncurl 'http://eureka-client.oto.tools:8080/port'\n$ 8082\n````\n\nThe configuration is ready and the setup is working, Otoroshi use all instances of your app to dispatch clients on it.\n\n### Connect to an external Eureka server\n\nOtoroshi has the possibility to discover services by connecting to an Eureka Server.\n\nLet's create a route with an Eureka application as Otoroshi target:\n\n* Create a new blank API route\n* Search and add the `External Eureka Server` plugin\n* Set your eureka URL\n* Click on `Fetch Services` button to discover the applications of the Eureka instance\n* In the appeared selector, choose the application to target\n* Once the frontend configured, save your route and try to call it.\n\nWell done, you have exposed your Eureka application through the Otoroshi discovery services.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"name":"zip-backend-plugin.md","id":"/how-to-s/zip-backend-plugin.md","url":"/how-to-s/zip-backend-plugin.html","title":"Quickly expose a website and static files ","content":"# Quickly expose a website and static files \n\n@@include[badge.md](../includes/badge.md) { #badge }\n\n## Tutorial\n\n1. [Before your start](#before-your-start)\n2. [Create an archive with HTML and CSS files](#create-an-archive-with-html-and-css-files)\n2. [Use the Zip Backend Plugin](#use-the-zip-backend-plugin)\n\nAfter completing these steps, you will be able to statically expose any kind of files from an archive.\n\n## Before your start\n\n@@include[initialize.md](../includes/initialize.md) { #initialize-otoroshi }\n\n## Create an archive with HTML and CSS files\n\nLet's start by creating an archive composed of html and css files.\n\nThe contents of your `index.html` file should be likes this:\n\n```html\n\n\n\n Wasmo plugin\n \n\n\n

      Hello from Wasmo

      \n\n\n```\n\nThe contents of your `index.css` file should be likes this:\n\n```css\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nOnce created, you can create the archive of both.\n\n```sh\nzip bundle.zip index.html index.css\n```\n\n## Use the Zip Backend Plugin \n\nLet's create the route using the Otoroshi admin API. The route content is pretty simple, a few fields about the name and the frontend, and the Zip Backend plugin in the plugins list.\n\nDon't forget to change the default `path-to-the-zip-file` with your path.\n\n``` sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"demootoroshi\",\n \"frontend\": {\n \"domains\": [\"demo-otoroshi.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"debug\": false,\n \"plugin\": \"cp:otoroshi.next.plugins.ZipFileBackend\",\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"url\": \"file:///bundle.zip\",\n \"headers\": {},\n \"dir\": \"./zips\",\n \"prefix\": null,\n \"ttl\": 3600000\n }\n }\n ]\n}\nEOF\n```\n\nCalling the route in a new browser tab at `http://demo-otoroshi.oto.tools:8080/`. You should see something like the following image:\n\n@@@ div { .centered-img }\n\n@@@\n\nAs we can see, the content of the archive is available, our HTML page is served and the CSS, linked into the HTML page, has loaded.\n\nYou can check this behaviour by calling the following path: \n\n```bash\ncurl http://demo-otoroshi.oto.tools:8080/index.css -v\n```\n\nThe result should be like:\n\n```bash\n< HTTP/1.1 200 OK\n< Transfer-Encoding: chunked\n< Content-Type: text/css\n<\nbody {\n background: #f9b000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100dvh;\n}\n\nh1 {\n font-size: 3rem;\n color: #fff;\n}\n```\n\nCongratulations - You have just exposed your first archive. Do not hesitate to expose any type of content."},{"name":"badge.md","id":"/includes/badge.md","url":"/includes/badge.html","title":"","content":"\n
      \nRoute plugins:\n
      Zip file backend plugin
      \n
      \n\n"},{"name":"experimental.md","id":"/includes/experimental.md","url":"/includes/experimental.html","title":"@@@ warning","content":"@@@ warning\n\nthis feature is **EXPERIMENTAL** and might not work as expected.
      \nIf you encounter any bugs, [please fill an issue](https://github.com/MAIF/otoroshi/issues/new), it will help us a lot :)\n\n@@@\n"},{"name":"fetch-and-start.md","id":"/includes/fetch-and-start.md","url":"/includes/fetch-and-start.html","title":"","content":"\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at @link:[http://otoroshi.oto.tools:8080](http://otoroshi.oto.tools:8080) { open=new } with `admin@otoroshi.io/password`\n"},{"name":"initialize.md","id":"/includes/initialize.md","url":"/includes/initialize.html","title":"","content":"\n\nIf you already have an up and running otoroshi instance, you can skip the following instructions\n\n\n@@@div { .instructions }\n\n
      \nSet up an Otoroshi\n\n
      \n\nLet's start by downloading the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n```\n\nthen you can run start Otoroshi :\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow you can log into Otoroshi at http://otoroshi.oto.tools:8080 with `admin@otoroshi.io/password`\n\nCreate a new route, exposed on `http://myservice.oto.tools:8080`, which will forward all requests to the mirror `https://mirror.otoroshi.io`. Each call to this service will returned the body and the headers received by the mirror.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"name\": \"my-service\",\n \"frontend\": {\n \"domains\": [\"myservice.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n }\n}\nEOF\n```\n\n\n@@@\n"},{"name":"index.md","id":"/index.md","url":"/index.html","title":"Otoroshi","content":"# Otoroshi\n\n**Otoroshi** is a layer of lightweight api management on top of a modern http reverse proxy written in Scala and developped by the MAIF OSS team that can handle all the calls to and between your microservices without service locator and let you change configuration dynamicaly at runtime.\n\n\n> *The Otoroshi is a large hairy monster that tends to lurk on the top of the torii gate in front of Shinto shrines. It's a hostile creature, but also said to be the guardian of the shrine and is said to leap down from the top of the gate to devour those who approach the shrine for only self-serving purposes.*\n\n@@@ div { .centered-img }\n[![Join the discord](https://img.shields.io/discord/1089571852940218538?color=f9b000&label=Community&logo=Discord&logoColor=f9b000)](https://discord.gg/dmbwZrfpcQ) [ ![Download](https://img.shields.io/github/release/MAIF/otoroshi.svg) ](hhttps://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar)\n@@@\n\n@@@ div { .centered-img }\n\n@@@\n\n## Installation\n\nYou can download the latest build of Otoroshi as a @ref:[fat jar](./install/get-otoroshi.md#from-jar-file), as a @ref:[zip package](./install/get-otoroshi.md#from-zip) or as a @ref:[docker image](./install/get-otoroshi.md#from-docker).\n\nYou can install and run Otoroshi with this little bash snippet\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\njava -jar otoroshi.jar\n```\n\nor using docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi:16.15.0-dev\n```\n\nnow open your browser to http://otoroshi.oto.tools:8080/, **log in with the credential generated in the logs** and explore by yourself, if you want better instructions, just go to the @ref:[Quick Start](./getting-started.md) or directly to the @ref:[installation instructions](./install/get-otoroshi.md)\n\n## Documentation\n\n* @ref:[About Otoroshi](./about.md)\n* @ref:[Architecture](./architecture.md)\n* @ref:[Features](./features.md)\n* @ref:[Getting started](./getting-started.md)\n* @ref:[Install Otoroshi](./install/index.md)\n* @ref:[Main entities](./entities/index.md)\n* @ref:[Detailed topics](./topics/index.md)\n* @ref:[How to's](./how-to-s/index.md)\n* @ref:[Plugins](./plugins/index.md)\n* @ref:[Admin REST API](./api.md)\n* @ref:[Deploy to production](./deploy/index.md)\n* @ref:[Developing Otoroshi](./dev.md)\n\n## Discussion\n\nJoin the @link:[Otoroshi server](https://discord.gg/dmbwZrfpcQ) { open=new } Discord\n\n## Sources\n\nThe sources of Otoroshi are available on @link:[Github](https://github.com/MAIF/otoroshi) { open=new }.\n\n## Logo\n\nYou can find the official Otoroshi logo @link:[on GitHub](https://github.com/MAIF/otoroshi/blob/master/resources/otoroshi-logo.png) { open=new }. The Otoroshi logo has been created by François Galioto ([@fgalioto](https://twitter.com/fgalioto))\n\n## Changelog\n\nEvery release, along with the migration instructions, is documented on the @link:[Github Releases](https://github.com/MAIF/otoroshi/releases) { open=new } page. A condensed version of the changelog is available on @link:[github](https://github.com/MAIF/otoroshi/blob/master/CHANGELOG.md) { open=new }\n\n## Patrons\n\nThe work on Otoroshi is funded by MAIF and Cloud APIM with the help of the community.\n\n## Licence\n\nOtoroshi is Open Source and available under the @link:[Apache 2 License](https://opensource.org/licenses/Apache-2.0) { open=new }\n\n@@@ index\n\n* [About Otoroshi](./about.md)\n* [Architecture](./architecture.md)\n* [Features](./features.md)\n* [Getting started](./getting-started.md)\n* [Install Otoroshi](./install/index.md)\n* [Main entities](./entities/index.md)\n* [Detailed topics](./topics/index.md)\n* [How to's](./how-to-s/index.md)\n* [Plugins](./plugins/index.md)\n* [Admin REST API](./api.md)\n* [Deploy to production](./deploy/index.md)\n* [Developing Otoroshi](./dev.md)\n* [Search doc](./search.md)\n\n@@@\n\n"},{"name":"get-otoroshi.md","id":"/install/get-otoroshi.md","url":"/install/get-otoroshi.html","title":"Get Otoroshi","content":"# Get Otoroshi\n\nAll release can be bound on the releases page of the @link:[repository](https://github.com/MAIF/otoroshi/releases) { open=new }.\n\n## From zip\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi-16.15.0-dev.zip\nunzip ./otoroshi-16.15.0-dev.zip\ncd otoroshi-16.15.0-dev\n```\n\n## From jar file\n\n```sh\n# Download the latest version\nwget https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar\n```\n\n## From Docker\n\n```sh\n# Download the latest version\ndocker pull maif/otoroshi:16.15.0-dev-jdk11\n```\n\n## From Sources\n\nTo build Otoroshi from sources, just go to the @ref:[dev documentation](../dev.md)\n"},{"name":"index.md","id":"/install/index.md","url":"/install/index.html","title":"Install","content":"# Install\n\nIn this sections, you will find informations about how to install and run Otoroshi\n\n* @ref:[Get Otoroshi](./get-otoroshi.md)\n* @ref:[Setup Otoroshi](./setup-otoroshi.md)\n* @ref:[Run Otoroshi](./run-otoroshi.md)\n\n@@@ index\n\n* [Get Otoroshi](./get-otoroshi.md)\n* [Setup Otoroshi](./setup-otoroshi.md)\n* [Run Otoroshi](./run-otoroshi.md)\n\n@@@\n"},{"name":"run-otoroshi.md","id":"/install/run-otoroshi.md","url":"/install/run-otoroshi.html","title":"Run Otoroshi","content":"# Run Otoroshi\n\nNow you are ready to run Otoroshi. You can run the following command with some tweaks depending on the way you want to configure Otoroshi. If you want to pass a custom configuration file, use the `-Dconfig.file=/path/to/file.conf` flag in the following commands.\n\n## From .zip file\n\n```sh\ncd otoroshi-vx.x.x\n./bin/otoroshi\n```\n\n## From .jar file\n\nFor Java 11\n\n```sh\njava -jar otoroshi.jar\n```\n\nif you want to run the jar file for on a JDK above JDK11, you'll have to add the following flags\n\n```sh\njava \\\n --add-opens=java.base/javax.net.ssl=ALL-UNNAMED \\\n --add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED \\\n --add-exports=java.base/sun.security.x509=ALL-UNNAMED \\\n --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \\\n -Dlog4j2.formatMsgNoLookups=true \\\n -jar otoroshi.jar\n```\n\n## From docker\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi\n```\n\nYou can also pass useful args like :\n\n```sh\ndocker run -p \"8080:8080\" maif/otoroshi -Dconfig.file=/usr/app/otoroshi/conf/otoroshi.conf -Dlogger.file=/usr/app/otoroshi/conf/otoroshi.xml\n```\n\nIf you want to provide your own config file, you can read @ref:[the documentation about config files](./setup-otoroshi.md).\n\nYou can also provide some ENV variable using the `--env` flag to customize your Otoroshi instance.\n\nThe list of possible env variables is available @ref:[here](./setup-otoroshi.md).\n\nYou can use a volume to provide configuration like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/conf\" maif/otoroshi\n```\n\nYou can also use a volume if you choose to use `filedb` datastore like :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd)/filedb:/usr/app/otoroshi/filedb\" maif/otoroshi -Dotoroshi.storage=file\n```\n\nYou can also use a volume if you choose to use exports files :\n\n```sh\ndocker run -p \"8080:8080\" -v \"$(pwd):/usr/app/otoroshi/imports\" maif/otoroshi -Dotoroshi.importFrom=/usr/app/otoroshi/imports/export.json\n```\n\n## Run examples\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -Dotoroshi.importFrom=/home/user/otoroshi.json \\\n -Dconfig.file=/home/user/otoroshi.conf \\\n -jar ./otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - Importing from: /home/user/otoroshi.json\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n\nIf you choose to start Otoroshi without importing existing data, Otoroshi will create a new admin user and print the login details in the log. When you will log into the admin dashboard, Otoroshi will ask you to create another account to avoid security issues.\n\n```sh\n$ java \\\n -Xms2G \\\n -Xmx8G \\\n -Dhttp.port=8080 \\\n -jar otoroshi.jar\n\n[warn] otoroshi-in-memory-datastores - Now using InMemory DataStores\n[warn] otoroshi-env - The main datastore seems to be empty, registering some basic services\n[warn] otoroshi-env - You can log into the Otoroshi admin console with the following credentials: admin@otoroshi.io / HHUsiF2UC3OPdmg0lGngEv3RrbIwWV5W\n[info] play.api.Play - Application started (Prod)\n[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:8080\n```\n"},{"name":"setup-otoroshi.md","id":"/install/setup-otoroshi.md","url":"/install/setup-otoroshi.html","title":"Setup Otoroshi","content":"# Setup Otoroshi\n\nin this section we are going to configure otoroshi before running it for the first time\n\n## Setup the database\n\nRight now, Otoroshi supports multiple datastore. You can choose one datastore over another depending on your use case.\n\n@@@div { .plugin .platform } \n
      Redis
      \n\n
      Recommended
      \n\nThe **redis** datastore is quite nice when you want to easily deploy several Otoroshi instances.\n\n\n\n@link:[Documentation](https://redis.io/topics/quickstart)\n@@@\n\n@@@div { .plugin .platform } \n
      In memory
      \n\nThe **in-memory** datastore is kind of interesting. It can be used for testing purposes, but it is also a good candidate for production because of its fastness.\n\n\n\n@ref:[Start with](../getting-started.md)\n@@@\n\n@@@div { .plugin .platform } \n
      Cassandra
      \n\n
      Clustering
      \n\nExperimental support, should be used in cluster mode for leaders\n\n\n\n@link:[Documentation](https://cassandra.apache.org/doc/latest/cassandra/getting_started/installing.html)\n@@@\n\n@@@div { .plugin .platform } \n
      Postgresql
      \n\n
      Clustering
      \n\nOr any postgresql compatible databse like cockroachdb for instance (experimental support, should be used in cluster mode for leaders)\n\n\n\n@link:[Documentation](https://www.postgresql.org/docs/10/tutorial-install.html)\n@@@\n\n@@@div { .plugin .platform } \n\n
      FileDB
      \n\nThe **filedb** datastore is pretty handy for testing purposes, but is not supposed to be used in production mode. \nNot suitable for production usage.\n\n\n\n@@@\n\n\n@@@ div { .centered-img }\n\n@@@\n\nthe first thing to setup is what kind of datastore you want to use with the `otoroshi.storage` setting\n\n```conf\notoroshi {\n storage = \"inmemory\" # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql \n}\n```\n\ndepending on the value you chose, you will be able to configure your datastore with the following configuration\n\ninmemory\n: @@snip [inmemory.conf](../snippets/datastores/inmemory.conf) \n\nfile\n: @@snip [file.conf](../snippets/datastores/file.conf) \n\nhttp\n: @@snip [http.conf](../snippets/datastores/http.conf) \n\ns3\n: @@snip [s3.conf](../snippets/datastores/s3.conf) \n\nredis\n: @@snip [lettuce.conf](../snippets/datastores/lettuce.conf) \n\npostgresql\n: @@snip [pg.conf](../snippets/datastores/pg.conf) \n\ncassandra\n: @@snip [inmemory.conf](../snippets/datastores/cassandra.conf) \n\n## Setup your hosts before running\n\nBy default, Otoroshi starts with domain `oto.tools` that automatically targets `127.0.0.1` with no changes to your `/etc/hosts` file. Of course you can change the domain value, you have to add the values in your `/etc/hosts` file according to the setting you put in Otoroshi configuration or define the right ip address at the DNS provider level\n\n* `otoroshi.domain` => `mydomain.org`\n* `otoroshi.backoffice.subdomain` => `otoroshi`\n* `otoroshi.privateapps.subdomain` => `privateapps`\n* `otoroshi.adminapi.exposedSubdomain` => `otoroshi-api`\n* `otoroshi.adminapi.targetSubdomain` => `otoroshi-admin-internal-api`\n\nfor instance if you want to change the default domain and use something like `otoroshi.mydomain.org`, then start otoroshi like \n\n```sh\njava -Dotoroshi.domain=mydomain.org -jar otoroshi.jar\n```\n\n@@@ warning\nOtoroshi cannot be accessed using `http://127.0.0.1:8080` or `http://localhost:8080` because Otoroshi uses Otoroshi to serve it's own UI and API. When otoroshi starts with an empty database, it will create a service descriptor for that using `otoroshi.domain` and the settings listed on this page and in the here that serve Otoroshi API and UI on `http://otoroshi-api.${otoroshi.domain}` and `http://otoroshi.${otoroshi.domain}`.\nOnce the descriptor is saved in database, if you want to change `otoroshi.domain`, you'll have to edit the descriptor in the database or restart Otoroshi with an empty database.\n@@@\n\n@@@ warning\nif your otoroshi instance runs behind a reverse proxy (L4 / L7) or inside a docker container where exposed ports (that you will use to access otoroshi) are not the same that the ones configured in otoroshi (`http.port` and `https.port`), you'll have to configure otoroshi exposed port to avoid bad redirection URLs when using authentication modules and other otoroshi tools. To do that, just set the values of the exposed ports in `otoroshi.exposed-ports.http = $theExposedHttpPort` (OTOROSHI_EXPOSED_PORTS_HTTP) and `otoroshi.exposed-ports.https = $theExposedHttpsPort` (OTOROSHI_EXPOSED_PORTS_HTTPS)\n@@@\n\n## Setup your configuration file\n\nThere is a lot of things you can configure in Otoroshi. By default, Otoroshi provides a configuration that should be enough for testing purpose. But you'll likely need to update this configuration when you'll need to move into production.\n\nIn this page, any configuration property can be set at runtime using a `-D` flag when launching Otoroshi like \n\n```sh\njava -Dhttp.port=8080 -jar otoroshi.jar\n```\n\nor\n\n```sh\n./bin/otoroshi -Dhttp.port=8080 \n```\n\nif you want to define your own config file and use it on an otoroshi instance, use the following flag\n\n```sh\njava -Dconfig.file=/path/to/otoroshi.conf -jar otoroshi.jar\n``` \n\n### Example of a custom. configuration file\n\n```conf\ninclude \"application.conf\"\n\nhttp.port = 8080\n\napp {\n storage = \"inmemory\"\n importFrom = \"./my-state.json\"\n env = \"prod\"\n domain = \"oto.tools\"\n rootScheme = \"http\"\n snowflake {\n seed = 0\n }\n events {\n maxSize = 1000\n }\n backoffice {\n subdomain = \"otoroshi\"\n session {\n exp = 86400000\n }\n }\n privateapps {\n subdomain = \"privateapps\"\n session {\n exp = 86400000\n }\n }\n adminapi {\n targetSubdomain = \"otoroshi-admin-internal-api\"\n exposedSubdomain = \"otoroshi-api\"\n defaultValues {\n backOfficeGroupId = \"admin-api-group\"\n backOfficeApiKeyClientId = \"admin-api-apikey-id\"\n backOfficeApiKeyClientSecret = \"admin-api-apikey-secret\"\n backOfficeServiceId = \"admin-api-service\"\n }\n }\n claim {\n sharedKey = \"mysecret\"\n }\n filedb {\n path = \"./filedb/state.ndjson\"\n }\n}\n\nplay.http {\n session {\n secure = false\n httpOnly = true\n maxAge = 2592000000\n domain = \".oto.tools\"\n cookieName = \"oto-sess\"\n }\n}\n```\n\n### Reference configuration\n\n@@snip [reference.conf](../snippets/reference.conf) \n\n### More config. options\n\nSee default configuration at\n\n* @link:[Base configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/base.conf) { open=new }\n* @link:[Application configuration](https://github.com/MAIF/otoroshi/blob/master/otoroshi/conf/application.conf) { open=new }\n\n## Configuration with env. variables\n\nEevery property in the configuration file can be overriden by an environment variable if it has env variable override written like `${?ENV_VARIABLE}`).\n\n## Reference configuration for env. variables\n\n@@snip [reference-env.conf](../snippets/reference-env.conf) \n"},{"name":"built-in-legacy-plugins.md","id":"/plugins/built-in-legacy-plugins.md","url":"/plugins/built-in-legacy-plugins.html","title":"Built-in legacy plugins","content":"# Built-in legacy plugins\n\nOtoroshi provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLog }\n\n## Access log (CLF)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in CLF format.\n\nLog format is the following:\n\n`\"$service\" $clientAddress - \"$userId\" [$timestamp] \"$host $method $path $protocol\" \"$status $statusTxt\" $size $snowflake \"$to\" \"$referer\" \"$userAgent\" $http $duration $errorMsg`\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.AccessLogJson }\n\n## Access log (JSON)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `AccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged in json format.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"AccessLog\": {\n \"enabled\": true,\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"AccessLog\" : {\n \"enabled\" : true,\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.accesslog.KafkaAccessLog }\n\n## Kafka access log\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `KafkaAccessLog`\n\n### Description\n\nWith this plugin, any access to a service will be logged as an event in a kafka topic.\n\nThe plugin accepts the following configuration\n\n```json\n{\n \"KafkaAccessLog\": {\n \"enabled\": true,\n \"topic\": \"otoroshi-access-log\",\n \"statuses\": [], // list of status to enable logs, if none, log everything\n \"paths\": [], // list of paths to enable logs, if none, log everything\n \"methods\": [], // list of http methods to enable logs, if none, log everything\n \"identities\": [] // list of identities to enable logs, if none, log everything\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KafkaAccessLog\" : {\n \"enabled\" : true,\n \"topic\" : \"otoroshi-access-log\",\n \"statuses\" : [ ],\n \"paths\" : [ ],\n \"methods\" : [ ],\n \"identities\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.BasicAuthCaller }\n\n## Basic Auth. caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\nThis plugin accepts the following configuration\n\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : \"the_username\",\n \"password\" : \"the_password\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.authcallers.OAuth2Caller }\n\n## OAuth2 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\nThis plugin accepts the following configuration\n\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"the oauth2 flow, can be 'client_credentials' or 'password'\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : \"an optional scope\",\n \"audience\" : \"an optional audience\",\n \"user\" : \"an optional username if using password flow\",\n \"password\" : \"an optional password if using password flow\",\n \"cacheTokenSeconds\" : \"the number of second to wait before asking for a new token\",\n \"tlsConfig\" : \"an optional TLS settings object\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.cache.ResponseCache }\n\n## Response Cache\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ResponseCache\": {\n \"enabled\": true, // enabled cache\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"autoClean\": true, // cleanup older keys when all bigger than maxSize\n \"filter\": { // cache only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ResponseCache\" : {\n \"enabled\" : true,\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.clientcert.ClientCertChainHeader }\n\n## Client certificate header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `ClientCertChain`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"ClientCertChain\": {\n \"pem\": { // send client cert as PEM format in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-Pem\"\n },\n \"dns\": { // send JSON array of DNs in a header\n \"send\": false,\n \"header\": \"X-Client-Cert-DNs\"\n },\n \"chain\": { // send JSON representation of client cert chain in a header\n \"send\": true,\n \"header\": \"X-Client-Cert-Chain\"\n },\n \"claims\": { // pass JSON representation of client cert chain in the otoroshi JWT token\n \"send\": false,\n \"name\": \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCertChain\" : {\n \"pem\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-Pem\"\n },\n \"dns\" : {\n \"send\" : false,\n \"header\" : \"X-Client-Cert-DNs\"\n },\n \"chain\" : {\n \"send\" : true,\n \"header\" : \"X-Client-Cert-Chain\"\n },\n \"claims\" : {\n \"send\" : false,\n \"name\" : \"clientCertChain\"\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.defer.DeferPlugin }\n\n## Defer Responses\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"DeferPlugin\": {\n \"defaultDefer\": 0 // default defer in millis\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"DeferPlugin\" : {\n \"defaultDefer\" : 0\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.discovery.DiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.geoloc.GeolocationInfoHeader }\n\n## Geolocation header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `GeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfoHeader\": {\n \"headerName\": \"X-Geolocation-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfoHeader\" : {\n \"headerName\" : \"X-Geolocation-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.hmac.HMACCallerPlugin }\n\n## HMAC caller plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HMACCallerPlugin`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACCallerPlugin\" : {\n \"secret\" : \"my-defaut-secret\",\n \"algo\" : \"HMAC-SHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiCanary }\n\n## Izanami Canary Campaign\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiCanary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiCanary\" : {\n \"experimentId\" : \"foo:bar:qix\",\n \"configId\" : \"foo:bar:qix:config\",\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000,\n \"mtls\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.izanami.IzanamiProxy }\n\n## Izanami APIs Proxy\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `IzanamiProxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"IzanamiProxy\" : {\n \"path\" : \"/api/izanami\",\n \"featurePattern\" : \"*\",\n \"configPattern\" : \"*\",\n \"autoContext\" : false,\n \"featuresEnabled\" : true,\n \"featuresWithContextEnabled\" : true,\n \"configurationEnabled\" : false,\n \"izanamiUrl\" : \"https://izanami.foo.bar\",\n \"izanamiClientId\" : \"client\",\n \"izanamiClientSecret\" : \"secret\",\n \"timeout\" : 5000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jq.JqBodyTransformer }\n\n## JQ bodies transformer\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `JqBodyTransformer`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\nSome JSON variables are accessible by default :\n\n * `$url`: the request url\n * `$path`: the request path\n * `$domain`: the request domain\n * `$method`: the request method\n * `$headers`: the current request headers (with name in lowercase)\n * `$queryParams`: the current request query params\n * `$otoToken`: the otoroshi protocol token (if one)\n * `$inToken`: the first matched JWT token as is (from verifiers, if one)\n * `$token`: the first matched JWT token as is (from verifiers, if one)\n * `$user`: the current user (if one)\n * `$apikey`: the current apikey (if one)\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"JqBodyTransformer\" : {\n \"request\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n },\n \"response\" : {\n \"filter\" : \".\",\n \"included\" : [ ],\n \"excluded\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.jsoup.HtmlPatcher }\n\n## Html Patcher\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `HtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"HtmlPatcher\" : {\n \"appendHead\" : [ ],\n \"appendBody\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.log4j.Log4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `Log4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them.\n\nThis plugin can accept the following configuration\n\n```javascript\n{\n \"Log4ShellFilter\": {\n \"status\": 200, // the status send back when an attack expression is found\n \"body\": \"\", // the body send back when an attack expression is found\n \"parseBody\": false // enables request body parsing to find attack expression\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"Log4ShellFilter\" : {\n \"status\" : 200,\n \"body\" : \"\",\n \"parseBody\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.loggers.BodyLogger }\n\n## Body logger\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `BodyLogger`\n\n### Description\n\nThis plugin can log body present in request and response. It can just logs it, store in in the redis store with a ttl and send it to analytics.\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"BodyLogger\": {\n \"enabled\": true, // enabled logging\n \"log\": true, // just log it\n \"store\": false, // store bodies in datastore\n \"ttl\": 300000, // store it for some times (5 minutes by default)\n \"sendToAnalytics\": false, // send bodies to analytics\n \"maxSize\": 5242880, // max body size (body will be cut after that)\n \"password\": \"password\", // password for the ui, if none, it's public\n \"filter\": { // log only for some status, method and paths\n \"statuses\": [],\n \"methods\": [],\n \"paths\": [],\n \"not\": {\n \"statuses\": [],\n \"methods\": [],\n \"paths\": []\n }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"BodyLogger\" : {\n \"enabled\" : true,\n \"log\" : true,\n \"store\" : false,\n \"ttl\" : 300000,\n \"sendToAnalytics\" : false,\n \"maxSize\" : 5242880,\n \"password\" : \"password\",\n \"filter\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ],\n \"not\" : {\n \"statuses\" : [ ],\n \"methods\" : [ ],\n \"paths\" : [ ]\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.mirror.MirroringPlugin }\n\n## Mirroring plugin\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `MirroringPlugin`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"MirroringPlugin\": {\n \"enabled\": true, // enabled mirroring\n \"to\": \"https://foo.bar.dev\", // the url of the service to mirror\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"MirroringPlugin\" : {\n \"enabled\" : true,\n \"to\" : \"https://foo.bar.dev\",\n \"captureResponse\" : false,\n \"generateEvents\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oauth1.OAuth1CallerPlugin }\n\n## OAuth1 caller\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"OAuth1Caller\" : {\n \"algo\" : \"HmacSHA512\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.oidc.OIDCHeaders }\n\n## OIDC headers\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCHeaders\" : {\n \"profile\" : {\n \"send\" : true,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idtoken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accesstoken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.security.SecurityTxt }\n\n## Security Txt\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `SecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/).\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"SecurityTxt\": {\n \"Contact\": \"contact@foo.bar\", // mandatory, a link or e-mail address for people to contact you about security issues\n \"Encryption\": \"http://url-to-public-key\", // optional, a link to a key which security researchers should use to securely talk to you\n \"Acknowledgments\": \"http://url\", // optional, a link to a web page where you say thank you to security researchers who have helped you\n \"Preferred-Languages\": \"en, fr, es\", // optional\n \"Policy\": \"http://url\", // optional, a link to a policy detailing what security researchers should do when searching for or reporting security issues\n \"Hiring\": \"http://url\", // optional, a link to any security-related job openings in your organisation\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"SecurityTxt\" : {\n \"Contact\" : \"contact@foo.bar\",\n \"Encryption\" : \"https://...\",\n \"Acknowledgments\" : \"https://...\",\n \"Preferred-Languages\" : \"en, fr\",\n \"Policy\" : \"https://...\",\n \"Hiring\" : \"https://...\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.static.StaticResponse }\n\n## Static Response\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `StaticResponse`\n\n### Description\n\nThis plugin returns a static response for any request\n\n\n\n### Default configuration\n\n```json\n{\n \"StaticResponse\" : {\n \"status\" : 200,\n \"headers\" : {\n \"Content-Type\" : \"application/json\"\n },\n \"body\" : \"{\\\"message\\\":\\\"hello world!\\\"}\",\n \"bodyBase64\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: ``none``\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint.\n\n`/.well-known/otoroshi/plugins/user-agent`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.useragent.UserAgentInfoHeader }\n\n## User-Agent header\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `UserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfoHeader\": {\n \"headerName\": \"X-User-Agent-Info\" // header in which info will be sent\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfoHeader\" : {\n \"headerName\" : \"X-User-Agent-Info\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-transformer #otoroshi.plugins.workflow.WorkflowEndpoint }\n\n## [DEPRECATED] Workflow endpoint\n\n\n\n### Infos\n\n* plugin type: `transformer`\n* configuration root: `WorkflowEndpoint`\n\n### Description\n\nThis plugin runs a workflow and return the response\n\n\n\n### Default configuration\n\n```json\n{\n \"WorkflowEndpoint\" : {\n \"workflow\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.biscuit.BiscuitValidator }\n\n## Biscuit token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nThis plugin validates a Biscuit token.\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nexpected response from http service is\n\n```json\n{\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n}\n```\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"url\": \"...\", // url for the call\n \"headers\": {}, // http header for the call\n \"ttl\": 600000, // cache ttl,\n \"mtlsConfig\": {\n \"certId\": \"xxxxx\",\n \"mtls\": false,\n \"loose\": false\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingHttpValidator\" : {\n \"url\" : \"http://foo.bar\",\n \"ttl\" : 600000,\n \"headers\" : { },\n \"mtlsConfig\" : {\n \"certId\" : \"...\",\n \"mtls\" : false,\n \"loose\" : false\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertMatchingValidator }\n\n## Client certificate matching\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\": {\n \"serialNumbers\": [], // allowed certificated serial numbers\n \"subjectDNs\": [], // allowed certificated DNs\n \"issuerDNs\": [], // allowed certificated issuer DNs\n \"regexSubjectDNs\": [], // allowed certificated DNs matching regex\n \"regexIssuerDNs\": [], // allowed certificated issuer DNs matching regex\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasClientCertMatchingValidator\" : {\n \"serialNumbers\" : [ ],\n \"subjectDNs\" : [ ],\n \"issuerDNs\" : [ ],\n \"regexSubjectDNs\" : [ ],\n \"regexIssuerDNs\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.clientcert.HasClientCertValidator }\n\n## Client Certificate Only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: ``none``\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.hmac.HMACValidator }\n\n## HMAC access validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HMACAccessValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"HMACAccessValidator\" : {\n \"secret\" : \"\"\n }\n}\n```\n\n\n\n### Documentation\n\n\n The HMAC signature needs to be set on the `Authorization` or `Proxy-Authorization` header.\n The format of this header should be : `hmac algorithm=\"\", headers=\"
      \", signature=\"\"`\n As example, a simple nodeJS call with the expected header\n ```js\n const crypto = require('crypto');\n const fetch = require('node-fetch');\n\n const date = new Date()\n const secret = \"my-secret\" // equal to the api key secret by default\n\n const algo = \"sha512\"\n const signature = crypto.createHmac(algo, secret)\n .update(date.getTime().toString())\n .digest('base64');\n\n fetch('http://myservice.oto.tools:9999/api/test', {\n headers: {\n \"Otoroshi-Client-Id\": \"my-id\",\n \"Otoroshi-Client-Secret\": \"my-secret\",\n \"Date\": date.getTime().toString(),\n \"Authorization\": `hmac algorithm=\"hmac-${algo}\", headers=\"Date\", signature=\"${signature}\"`,\n \"Accept\": \"application/json\"\n }\n })\n .then(r => r.json())\n .then(console.log)\n ```\n In this example, we have an Otoroshi service deployed on http://myservice.oto.tools:9999/api/test, protected by api keys.\n The secret used is the secret of the api key (by default, but you can change it and define a secret on the plugin configuration).\n We send the base64 encoded date of the day, signed by the secret, in the Authorization header. We specify the headers signed and the type of algorithm used.\n You can sign more than one header but you have to list them in the headers fields (each one separate by a space, example : headers=\"Date KeyId\").\n The algorithm used can be HMAC-SHA1, HMAC-SHA256, HMAC-SHA384 or HMAC-SHA512.\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.oidc.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.quotas.ServiceQuotas }\n\n## Public quotas\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `ServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current service\n\n\n\n\n\n\n\n### Default configuration\n\n```json\n{\n \"ServiceQuotas\" : {\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-validator #otoroshi.plugins.users.HasAllowedUsersValidator }\n\n## Allowed users only\n\n\n\n### Infos\n\n* plugin type: `validator`\n* configuration root: `HasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"HasAllowedUsersValidator\": {\n \"usernames\": [], // allowed usernames\n \"emails\": [], // allowed user email addresses\n \"emailDomains\": [], // allowed user email domains\n \"metadataMatch\": [], // json path expressions to match against user metadata. passes if one match\n \"metadataNotMatch\": [], // json path expressions to match against user metadata. passes if none match\n \"profileMatch\": [], // json path expressions to match against user profile. passes if one match\n \"profileNotMatch\": [], // json path expressions to match against user profile. passes if none match\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"HasAllowedUsersValidator\" : {\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"emailDomains\" : [ ],\n \"metadataMatch\" : [ ],\n \"metadataNotMatch\" : [ ],\n \"profileMatch\" : [ ],\n \"profileNotMatch\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ApikeyAuthModule }\n\n## Apikey auth module\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"ApikeyAuthModule\" : {\n \"realm\" : \"apikey-auth-module-realm\",\n \"noneTagIn\" : [ ],\n \"oneTagIn\" : [ ],\n \"allTagsIn\" : [ ],\n \"noneMetaIn\" : [ ],\n \"oneMetaIn\" : [ ],\n \"allMetaIn\" : [ ],\n \"noneMetaKeysIn\" : [ ],\n \"oneMetaKeyIn\" : [ ],\n \"allMetaKeysIn\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.CertificateAsApikey }\n\n## Client certificate as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `CertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"CertificateAsApikey\" : {\n \"readOnly\" : false,\n \"allowClientIdOnly\" : false,\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"constrainedServicesOnly\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.apikeys.ClientCredentialFlowExtractor }\n\n## Client Credential Flow ApiKey extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin can extract an apikey from an opaque access_token generate by the `ClientCredentialFlow` plugin\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.biscuit.BiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: ``none``\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"publicKey\" : \"xxxxxx\",\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"enforce\" : false,\n \"extractor\" : {\n \"type\" : \"header\",\n \"name\" : \"Authorization\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.discovery.DiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.IpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"apikey\": \"xxxxxxx\",\n \"timeout\": 2000, // timeout in ms\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"apikey\" : \"xxxxxxx\",\n \"timeout\" : 2000,\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.geoloc.MaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `GeolocationInfo`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"GeolocationInfo\": {\n \"path\": \"/foo/bar/cities.mmdb\", // file path, can be \"global\"\n \"log\": false // will log geolocation details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"GeolocationInfo\" : {\n \"path\" : \"global\",\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.jwt.JwtUserExtractor }\n\n## Jwt user extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `JwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"JwtUserExtractor\" : {\n \"verifier\" : \"\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\" : \"email\",\n \"metaPath\" : null\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.oidc.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"OIDCAccessTokenValidator\": {\n \"enabled\": true,\n \"atLeastOne\": false,\n // config is optional and can be either an object config or an array of objects\n \"config\": {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n}\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"OIDCAccessTokenAsApikey\" : {\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-preroute #otoroshi.plugins.useragent.UserAgentExtractor }\n\n## User-Agent details extractor\n\n\n\n### Infos\n\n* plugin type: `preroute`\n* configuration root: `UserAgentInfo`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\nThis plugin can accept the following configuration\n\n```json\n{\n \"UserAgentInfo\": {\n \"log\": false // will log user-agent details\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"UserAgentInfo\" : {\n \"log\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.apikeys.ClientCredentialService }\n\n## Client Credential Service\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `ClientCredentialService`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"ClientCredentialService\" : {\n \"domain\" : \"*\",\n \"expiration\" : 3600000,\n \"defaultKeyPair\" : \"otoroshi-jwt-signing\",\n \"secure\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.discovery.DiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: `DiscoverySelfRegistration`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames.\n\nThis plugin accepts the following configuration:\n\n\n\n### Default configuration\n\n```json\n{\n \"DiscoverySelfRegistration\" : {\n \"hosts\" : [ ],\n \"targetTemplate\" : { },\n \"registrationTtl\" : 60000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookCRDValidator }\n\n## Kubernetes admission validator webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to handle manifests validation\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-sink #otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector }\n\n## Kubernetes sidecar injector webhook\n\n\n\n### Infos\n\n* plugin type: `sink`\n* configuration root: ``none``\n\n### Description\n\nThis plugin exposes a webhook to kubernetes to inject otoroshi-sidecar in pods\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.jobs.StateExporter }\n\n## Otoroshi state exporter job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `StateExporter`\n\n### Description\n\nThis job send an event containing the full otoroshi export every n seconds\n\n\n\n### Default configuration\n\n```json\n{\n \"StateExporter\" : {\n \"every_sec\" : 3600,\n \"format\" : \"json\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleCertificatesFetcherJob }\n\n## Tailscale certificate fetcher job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will fetch certificates from Tailscale ACME provider\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.next.plugins.TailscaleTargetsJob }\n\n## Tailscale targets job\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n### Description\n\nThis job will aggregates Tailscale possible online targets\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesIngressControllerJob }\n\n## Kubernetes Ingress Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi as an Ingress Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesOtoroshiCRDsControllerJob }\n\n## Kubernetes Otoroshi CRDs Controller\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin enables Otoroshi CRDs Controller\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.KubernetesToOtoroshiCertSyncJob }\n\n## Kubernetes to Otoroshi certs. synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. TLS secrets from Kubernetes to Otoroshi\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.plugins.jobs.kubernetes.OtoroshiToKubernetesCertSyncJob }\n\n## Otoroshi certs. to Kubernetes secrets synchronizer\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: `KubernetesConfig`\n\n### Description\n\nThis plugin syncs. Otoroshi certs to Kubernetes TLS secrets\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"KubernetesConfig\" : {\n \"endpoint\" : \"https://kube.cluster.dev\",\n \"token\" : \"xxx\",\n \"userPassword\" : \"user:password\",\n \"caCert\" : \"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\n \"trust\" : false,\n \"namespaces\" : [ \"*\" ],\n \"labels\" : { },\n \"namespacesLabels\" : { },\n \"ingressClasses\" : [ \"otoroshi\" ],\n \"defaultGroup\" : \"default\",\n \"ingresses\" : true,\n \"crds\" : true,\n \"coreDnsIntegration\" : false,\n \"coreDnsIntegrationDryRun\" : false,\n \"coreDnsAzure\" : false,\n \"kubeLeader\" : false,\n \"restartDependantDeployments\" : true,\n \"useProxyState\" : false,\n \"watch\" : true,\n \"syncDaikokuApikeysOnly\" : false,\n \"kubeSystemNamespace\" : \"kube-system\",\n \"coreDnsConfigMapName\" : \"coredns\",\n \"coreDnsDeploymentName\" : \"coredns\",\n \"corednsPort\" : 53,\n \"otoroshiServiceName\" : \"otoroshi-service\",\n \"otoroshiNamespace\" : \"otoroshi\",\n \"clusterDomain\" : \"cluster.local\",\n \"syncIntervalSeconds\" : 60,\n \"coreDnsEnv\" : null,\n \"watchTimeoutSeconds\" : 60,\n \"watchGracePeriodSeconds\" : 5,\n \"mutatingWebhookName\" : \"otoroshi-admission-webhook-injector\",\n \"validatingWebhookName\" : \"otoroshi-admission-webhook-validation\",\n \"meshDomain\" : \"otoroshi.mesh\",\n \"openshiftDnsOperatorIntegration\" : false,\n \"openshiftDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"openshiftDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"openshiftDnsOperatorCoreDnsPort\" : 5353,\n \"kubeDnsOperatorIntegration\" : false,\n \"kubeDnsOperatorCoreDnsNamespace\" : \"otoroshi\",\n \"kubeDnsOperatorCoreDnsName\" : \"otoroshi-dns\",\n \"kubeDnsOperatorCoreDnsPort\" : 5353,\n \"connectionTimeout\" : 5000,\n \"idleTimeout\" : 30000,\n \"callAndStreamTimeout\" : 30000,\n \"templates\" : {\n \"service-group\" : { },\n \"service-descriptor\" : { },\n \"apikeys\" : { },\n \"global-config\" : { },\n \"jwt-verifier\" : { },\n \"tcp-service\" : { },\n \"certificate\" : { },\n \"auth-module\" : { },\n \"script\" : { },\n \"data-exporters\" : { },\n \"organizations\" : { },\n \"teams\" : { },\n \"admins\" : { },\n \"webhooks\" : { }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-job #otoroshi.wasm.WasmVmPoolCleaner }\n\n## otoroshi.wasm.WasmVmPoolCleaner\n\n\n\n### Infos\n\n* plugin type: `job`\n* configuration root: ``none``\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.next.proxy.ProxyEngine }\n\n## Otoroshi next proxy engine (experimental)\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `NextGenProxyEngine`\n\n### Description\n\nThis plugin holds the next generation otoroshi proxy engine implementation. This engine is **experimental** and may not work as expected !\n\nYou can active this plugin only on some domain names so you can easily A/B test the new engine.\nThe new proxy engine is designed to be more reactive and more efficient generally.\nIt is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nThe idea is to only rely on plugins to work and avoid losing time with features that are not used in service descriptors.\nAn automated conversion happens for every service descriptor. If the exposed domain is handled by this plugin, it will be served by this plugin.\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `route-compositions`: multiple routing rules based on hostname, path, method and headers that will execute the same list of plugins\n - `backends`: a list of targets to contact a backend\n\nas an example, let say you want to use the new engine on your service exposed on `api.foo.bar/api`.\nTo do that, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration,\nenabled it and in `domains` add the value `api.foo.bar` (it is possible to use `*.foo.bar` if that's what you want to do).\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the old one.\n\n\n\n### Default configuration\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"domains\" : [ \"*\" ],\n \"deny_domains\" : [ ],\n \"reporting\" : true,\n \"merge_sync_steps\" : true,\n \"export_reporting\" : false,\n \"apply_legacy_checks\" : true,\n \"debug\" : false,\n \"capture\" : false,\n \"captureMaxEntitySize\" : 4194304,\n \"debug_headers\" : false,\n \"routing_strategy\" : \"tree\"\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .plugin .plugin-hidden .plugin-kind-request-handler #otoroshi.script.ForwardTrafficHandler }\n\n## Forward traffic\n\n\n\n### Infos\n\n* plugin type: `request-handler`\n* configuration root: `ForwardTrafficHandler`\n\n### Description\n\nThis plugin can be use to perform a raw traffic forward to an URL without passing through otoroshi routing\n\n\n\n### Default configuration\n\n```json\n{\n \"ForwardTrafficHandler\" : {\n \"domains\" : {\n \"my.domain.tld\" : {\n \"baseUrl\" : \"https://my.otherdomain.tld\",\n \"secret\" : \"jwt signing secret\",\n \"service\" : {\n \"id\" : \"service id for analytics\",\n \"name\" : \"service name for analytics\"\n }\n }\n }\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n\n\n"},{"name":"built-in-plugins.md","id":"/plugins/built-in-plugins.md","url":"/plugins/built-in-plugins.html","title":"Built-in plugins","content":"# Built-in plugins\n\nOtoroshi next provides some plugins out of the box. Here is the available plugins with their documentation and reference configuration.\n\n
      \n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersIn }\n\n## Additional headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersIn`\n\n### Description\n\nThis plugin adds headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AdditionalHeadersOut }\n\n## Additional headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AdditionalHeadersOut`\n\n### Description\n\nThis plugin adds headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AllowHttpMethods }\n\n## Allowed HTTP methods\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AllowHttpMethods`\n\n### Description\n\nThis plugin verifies the current request only uses allowed http methods\n\n\n\n### Default configuration\n\n```json\n{\n \"allowed\" : [ ],\n \"forbidden\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyAuthModule }\n\n## Apikey auth module\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyAuthModule`\n\n### Description\n\nThis plugin adds basic auth on service where credentials are valid apikeys on the current service.\n\n\n\n### Default configuration\n\n```json\n{\n \"realm\" : \"apikey-auth-module-realm\",\n \"matcher\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyCalls }\n\n## Apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyCalls`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass\n\n\n\n### Default configuration\n\n```json\n{\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ApikeyQuotas }\n\n## Apikey quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ApikeyQuotas`\n\n### Description\n\nIncrements quotas for the currents apikey. Useful when 'legacy checks' are disabled on a service/globally or when apikey are extracted in a custom fashion.\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.AuthModule }\n\n## Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.AuthModule`\n\n### Description\n\nThis plugin applies an authentication module\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BasicAuthCaller }\n\n## Basic Auth. caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BasicAuthCaller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using basic auth.\n\n\n\n### Default configuration\n\n```json\n{\n \"username\" : null,\n \"passaword\" : null,\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Basic %s\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BrotliResponseCompressor }\n\n## Brotli compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BrotliResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using brotli\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.BuildMode }\n\n## Build mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.BuildMode`\n\n### Description\n\nThis plugin displays a build page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.CanaryMode }\n\n## Canary mode\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.CanaryMode`\n\n### Description\n\nThis plugin can split a portion of the traffic to canary backends\n\n\n\n### Default configuration\n\n```json\n{\n \"traffic\" : 0.2,\n \"targets\" : [ ],\n \"root\" : \"/\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ContextValidation }\n\n## Context validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ContextValidation`\n\n### Description\n\nThis plugin validates the current context using JSONPath validators.\n\nThis plugin let you configure a list of validators that will check if the current call can pass.\nA validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value.\nThe JSONPath will be applied on a document that will look like\n\n```js\n{\n \"snowflake\" : \"1516772930422308903\",\n \"apikey\" : { // current apikey\n \"clientId\" : \"vrmElDerycXrofar\",\n \"clientName\" : \"default-apikey\",\n \"metadata\" : {\n \"foo\" : \"bar\"\n },\n \"tags\" : [ ]\n },\n \"user\" : null, // current user\n \"request\" : {\n \"id\" : 1,\n \"method\" : \"GET\",\n \"headers\" : {\n \"Host\" : \"ctx-validation-next-gen.oto.tools:9999\",\n \"Accept\" : \"*/*\",\n \"User-Agent\" : \"curl/7.64.1\",\n \"Authorization\" : \"Basic dnJtRWxEZXJ5Y1hyb2ZhcjpvdDdOSTkyVGI2Q2J4bWVMYU9UNzJxamdCU2JlRHNLbkxtY1FBcXBjVjZTejh0Z3I1b2RUOHAzYjB5SEVNRzhZ\",\n \"Remote-Address\" : \"127.0.0.1:58929\",\n \"Timeout-Access\" : \"\",\n \"Raw-Request-URI\" : \"/foo\",\n \"Tls-Session-Info\" : \"Session(1650461821330|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\" : [ ],\n \"tls\" : false,\n \"uri\" : \"/foo\",\n \"path\" : \"/foo\",\n \"version\" : \"HTTP/1.1\",\n \"has_body\" : false,\n \"remote\" : \"127.0.0.1\",\n \"client_cert_chain\" : null\n },\n \"config\" : {\n \"validators\" : [ {\n \"path\" : \"$.apikey.metadata.foo\",\n \"value\" : \"bar\"\n } ]\n },\n \"global_config\" : { ... }, // global config\n \"attrs\" : {\n \"otoroshi.core.SnowFlake\" : \"1516772930422308903\",\n \"otoroshi.core.ElCtx\" : {\n \"requestId\" : \"1516772930422308903\",\n \"requestSnowflake\" : \"1516772930422308903\",\n \"requestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\"\n },\n \"otoroshi.next.core.Report\" : \"otoroshi.next.proxy.NgExecutionReport@277b44e2\",\n \"otoroshi.core.RequestStart\" : 1650461821545,\n \"otoroshi.core.RequestWebsocket\" : false,\n \"otoroshi.core.RequestCounterOut\" : 0,\n \"otoroshi.core.RemainingQuotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 0,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 2,\n \"remainingCallsPerDay\" : 9999998,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 269,\n \"remainingCallsPerMonth\" : 9999731\n },\n \"otoroshi.next.core.MatchedRoutes\" : \"MutableList(route_022825450-e97d-42ed-8e22-b23342c1c7c8)\",\n \"otoroshi.core.RequestNumber\" : 1,\n \"otoroshi.next.core.Route\" : { ... }, // current route as json\n \"otoroshi.core.RequestTimestamp\" : \"2022-04-20T15:37:01.548+02:00\",\n \"otoroshi.core.ApiKey\" : { ... }, // current apikey as json\n \"otoroshi.core.User\" : { ... }, // current user as json\n \"otoroshi.core.RequestCounterIn\" : 0\n },\n \"route\" : { ... },\n \"token\" : null // current valid jwt token if one\n}\n```\n\nthe expected value support some syntax tricks like\n\n* `Not(value)` on a string to check if the current value does not equals another value\n* `Regex(regex)` on a string to check if the current value matches the regex\n* `RegexNot(regex)` on a string to check if the current value does not matches the regex\n* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards\n* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards\n* `Contains(value)` on a string to check if the current value contains a value\n* `ContainsNot(value)` on a string to check if the current value does not contains a value\n* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex\n* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex\n* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value\n* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value\n* `Contains(value)` on an array to check if the array contains a value\n* `ContainsNot(value)` on an array to check if the array does not contains a value\n\nfor instance to check if the current apikey has a metadata name `foo` with a value containing `bar`, you can write the following validator\n\n```js\n{\n \"path\": \"$.apikey.metadata.foo\",\n \"value\": \"Contains(bar)\"\n}\n```\n\n\n\n### Default configuration\n\n```json\n{\n \"validators\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Cors }\n\n## CORS\n\n### Defined on steps\n\n - `PreRoute`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Cors`\n\n### Description\n\nThis plugin applies CORS rules\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_origin\" : \"*\",\n \"expose_headers\" : [ ],\n \"allow_headers\" : [ ],\n \"allow_methods\" : [ ],\n \"excluded_patterns\" : [ ],\n \"max_age\" : null,\n \"allow_credentials\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.DisableHttp10 }\n\n## Disable HTTP/1.0\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.DisableHttp10`\n\n### Description\n\nThis plugin forbids HTTP/1.0 requests\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EndlessHttpResponse }\n\n## Endless HTTP responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EndlessHttpResponse`\n\n### Description\n\nThis plugin returns 128 Gb of 0 to the ip addresses is in the list\n\n\n\n### Default configuration\n\n```json\n{\n \"finger\" : false,\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaServerSink }\n\n## Eureka instance\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaServerSink`\n\n### Description\n\nEureka plugin description\n\n\n\n### Default configuration\n\n```json\n{\n \"evictionTimeout\" : 300\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.EurekaTarget }\n\n## Internal Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.EurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an internal Eureka server.\n If you want to use a target which it locate outside of Otoroshi, you must use the External Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ExternalEurekaTarget }\n\n## External Eureka target\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ExternalEurekaTarget`\n\n### Description\n\nThis plugin can be used to used a target that come from an external Eureka server.\n If you want to use a target that is directly exposed by an implementation of Eureka by Otoroshi,\n you must use the Internal Eureka Server.\n\n\n\n### Default configuration\n\n```json\n{\n \"eureka_server\" : null,\n \"eureka_app\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForceHttpsTraffic }\n\n## Force HTTPS traffic\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForceHttpsTraffic`\n\n### Description\n\nThis plugin verifies the current request uses HTTPS\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ForwardedHeader }\n\n## Forwarded header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ForwardedHeader`\n\n### Description\n\nThis plugin adds all the Forwarded header to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalMaintenanceMode }\n\n## Global Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalMaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page for every services. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalPerIpAddressThrottling }\n\n## Global per ip address throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalPerIpAddressThrottling`\n\n### Description\n\nEnforce global per ip address throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GlobalThrottling }\n\n## Global throttling \n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GlobalThrottling`\n\n### Description\n\nEnforce global throttling. Useful when 'legacy checks' are disabled on a service/globally\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLBackend }\n\n## GraphQL Composer\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLBackend`\n\n### Description\n\nThis plugin exposes a GraphQL API that you can compose with whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"schema\" : \"\\n type User {\\n name: String!\\n firstname: String!\\n }\\n\\n type Query {\\n users: [User] @json(data: \\\"[{ \\\\\\\"firstname\\\\\\\": \\\\\\\"Foo\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Bar\\\\\\\" }, { \\\\\\\"firstname\\\\\\\": \\\\\\\"Bar\\\\\\\", \\\\\\\"name\\\\\\\": \\\\\\\"Foo\\\\\\\" }]\\\")\\n }\\n \",\n \"permissions\" : [ ],\n \"initial_data\" : null,\n \"max_depth\" : 15\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLProxy }\n\n## GraphQL Proxy\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLProxy`\n\n### Description\n\nThis plugin can apply validations (query, schema, max depth, max complexity) on graphql endpoints\n\n\n\n### Default configuration\n\n```json\n{\n \"endpoint\" : \"https://countries.trevorblades.com/graphql\",\n \"schema\" : null,\n \"max_depth\" : 50,\n \"max_complexity\" : 50000,\n \"path\" : \"/graphql\",\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GraphQLQuery }\n\n## GraphQL Query to REST\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GraphQLQuery`\n\n### Description\n\nThis plugin can be used to call GraphQL query endpoints and expose it as a REST endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://some.graphql/endpoint\",\n \"headers\" : { },\n \"method\" : \"POST\",\n \"query\" : \"{\\n\\n}\",\n \"timeout\" : 60000,\n \"response_path\" : null,\n \"response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.GzipResponseCompressor }\n\n## Gzip compression\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.GzipResponseCompressor`\n\n### Description\n\nThis plugin can compress responses using gzip\n\n\n\n### Default configuration\n\n```json\n{\n \"excluded_patterns\" : [ ],\n \"allowed_list\" : [ \"text/*\", \"application/javascript\", \"application/json\" ],\n \"blocked_list\" : [ ],\n \"buffer_size\" : 8192,\n \"chunked_threshold\" : 102400,\n \"compression_level\" : 5\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACCaller }\n\n## HMAC caller plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACCaller`\n\n### Description\n\nThis plugin can be used to call a \"protected\" api by an HMAC signature. It will adds a signature with the secret configured on the plugin.\n The signature string will always the content of the header list listed in the plugin configuration.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null,\n \"algo\" : \"HMAC-SHA512\",\n \"authorizationHeader\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HMACValidator }\n\n## HMAC access validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HMACValidator`\n\n### Description\n\nThis plugin can be used to check if a HMAC signature is present and valid in Authorization header.\n\n\n\n### Default configuration\n\n```json\n{\n \"secret\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.HeadersValidation }\n\n## Headers validation\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.HeadersValidation`\n\n### Description\n\nThis plugin validates the values of incoming request headers\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Http3Switch }\n\n## Http3 traffic switch\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Http3Switch`\n\n### Description\n\nThis plugin injects additional alt-svc header to switch to the http3 server\n\n\n\n### Default configuration\n\n```json\n{\n \"ma\" : 3600\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ImageReplacer }\n\n## Image replacer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ImageReplacer`\n\n### Description\n\nReplace all response with content-type image/* as they are proxied\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://raw.githubusercontent.com/MAIF/otoroshi/master/resources/otoroshi-logo.png\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressAllowedList }\n\n## IP allowed list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressAllowedList`\n\n### Description\n\nThis plugin verifies the current request ip address is in the allowed list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.IpAddressBlockList }\n\n## IP block list\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.IpAddressBlockList`\n\n### Description\n\nThis plugin verifies the current request ip address is not in the blocked list\n\n\n\n### Default configuration\n\n```json\n{\n \"addresses\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQ }\n\n## JQ\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQ`\n\n### Description\n\nThis plugin let you transform JSON bodies (in requests and responses) using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"request\" : \".\",\n \"response\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQRequest }\n\n## JQ transform request\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQRequest`\n\n### Description\n\nThis plugin let you transform request JSON body using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JQResponse }\n\n## JQ transform response\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JQResponse`\n\n### Description\n\nThis plugin let you transform JSON response using [JQ filters](https://stedolan.github.io/jq/manual/#Basicfilters).\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : \".\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlRequest }\n\n## request body json-to-xml\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlRequest`\n\n### Description\n\nThis plugin transform incoming request body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JsonToXmlResponse }\n\n## response body json-to-xml\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JsonToXmlResponse`\n\n### Description\n\nThis plugin transform response body from json to xml and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtSigner }\n\n## Jwt signer\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtSigner`\n\n### Description\n\nThis plugin can only generate token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"replace_if_present\" : true,\n \"fail_if_present\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerification }\n\n## Jwt verifiers\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerification`\n\n### Description\n\nThis plugin verifies the current request with one or more jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifiers\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.JwtVerificationOnly }\n\n## Jwt verification only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.JwtVerificationOnly`\n\n### Description\n\nThis plugin verifies the current request with one jwt verifier\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : null,\n \"fail_if_absent\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MaintenanceMode }\n\n## Maintenance mode\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MaintenanceMode`\n\n### Description\n\nThis plugin displays a maintenance page\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersIn }\n\n## Missing headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersIn`\n\n### Description\n\nThis plugin adds headers (if missing) in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MissingHeadersOut }\n\n## Missing headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MissingHeadersOut`\n\n### Description\n\nThis plugin adds headers (if missing) in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"headers\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MockResponses }\n\n## Mock Responses\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MockResponses`\n\n### Description\n\nThis plugin returns mock responses\n\n\n\n### Default configuration\n\n```json\n{\n \"responses\" : [ ],\n \"pass_through\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.MultiAuthModule }\n\n## Multi Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.MultiAuthModule`\n\n### Description\n\nThis plugin applies an authentication module from a list of selected modules\n\n\n\n### Default configuration\n\n```json\n{\n \"pass_with_apikey\" : false,\n \"auth_modules\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleExpectedUser }\n\n## User logged in expected\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleExpectedUser`\n\n### Description\n\nThis plugin enforce that a user from any auth. module is logged in\n\n\n\n### Default configuration\n\n```json\n{\n \"only_from\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgAuthModuleUserExtractor }\n\n## User extraction from auth. module\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgAuthModuleUserExtractor`\n\n### Description\n\nThis plugin extracts users from an authentication module without enforcing login\n\n\n\n### Default configuration\n\n```json\n{\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitExtractor }\n\n## Apikey from Biscuit token extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitExtractor`\n\n### Description\n\nThis plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgBiscuitValidator }\n\n## Biscuit token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgBiscuitValidator`\n\n### Description\n\nThis plugin validates a Biscuit token\n\n\n\n### Default configuration\n\n```json\n{\n \"public_key\" : null,\n \"checks\" : [ ],\n \"facts\" : [ ],\n \"resources\" : [ ],\n \"rules\" : [ ],\n \"revocation_ids\" : [ ],\n \"extractor\" : {\n \"name\" : \"Authorization\",\n \"type\" : \"header\"\n },\n \"enforce\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCertificateAsApikey }\n\n## Client certificate as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCertificateAsApikey`\n\n### Description\n\nThis plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage\n\n\n\n### Default configuration\n\n```json\n{\n \"read_only\" : false,\n \"allow_client_id_only\" : false,\n \"throttling_quota\" : 100,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000,\n \"constrained_services_only\" : false,\n \"tags\" : [ ],\n \"metadata\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCertChainHeader }\n\n## Client certificate header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCertChainHeader`\n\n### Description\n\nThis plugin pass client certificate informations to the target in headers\n\n\n\n### Default configuration\n\n```json\n{\n \"send_pem\" : false,\n \"pem_header_name\" : \"X-Client-Cert-Pem\",\n \"send_dns\" : false,\n \"dns_header_name\" : \"X-Client-Cert-DNs\",\n \"send_chain\" : false,\n \"chain_header_name\" : \"X-Client-Cert-Chain\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentialTokenEndpoint }\n\n## Client credential token endpoint\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentialTokenEndpoint`\n\n### Description\n\nThis plugin provide the endpoint for the client_credential flow token endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgClientCredentials }\n\n## Client Credential Service\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgClientCredentials`\n\n### Description\n\nThis plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret\n\n\n\n### Default configuration\n\n```json\n{\n \"expiration\" : 3600000,\n \"default_key_pair\" : \"otoroshi-jwt-signing\",\n \"domain\" : \"*\",\n \"secure\" : true,\n \"biscuit\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomQuotas }\n\n## Custom quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomQuotas`\n\n### Description\n\nThis plugin will enforce quotas on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgCustomThrottling }\n\n## Custom throttling\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgCustomThrottling`\n\n### Description\n\nThis plugin will enforce throttling on the current route based on whatever you want\n\n\n\n### Default configuration\n\n```json\n{\n \"per_route\" : true,\n \"global\" : false,\n \"group\" : null,\n \"expression\" : \"${req.ip}\",\n \"throttling_quota\" : 100\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDefaultRequestBody }\n\n## Default request body\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDefaultRequestBody`\n\n### Description\n\nThis plugin adds a default request body if none specified\n\n\n\n### Default configuration\n\n```json\n{\n \"bodyBinary\" : \"\",\n \"contentType\" : \"text/plain\",\n \"contentEncoding\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDeferPlugin }\n\n## Defer Responses\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDeferPlugin`\n\n### Description\n\nThis plugin will expect a `X-Defer` header or a `defer` query param and defer the response according to the value in milliseconds.\nThis plugin is some kind of inside joke as one a our customer ask us to make slower apis.\n\n\n\n### Default configuration\n\n```json\n{\n \"duration\" : 0\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationSink }\n\n## Global self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationSink`\n\n### Description\n\nThis plugin add support for self registration endpoint on specific hostnames\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer }\n\n## Self registration endpoints (service discovery)\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoverySelfRegistrationTransformer`\n\n### Description\n\nThis plugin add support for self registration endpoint on a specific service\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgDiscoveryTargetsSelector }\n\n## Service discovery target selector (service discovery)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgDiscoveryTargetsSelector`\n\n### Description\n\nThis plugin select a target in the pool of discovered targets for this service.\nUse in combination with either `DiscoverySelfRegistrationSink` or `DiscoverySelfRegistrationTransformer` to make it work using the `self registration` pattern.\nOr use an implementation of `DiscoveryJob` for the `third party registration pattern`.\n\n\n\n### Default configuration\n\n```json\n{ }\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgErrorRewriter }\n\n## Error response rewrite\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgErrorRewriter`\n\n### Description\n\nThis plugin catch http response with specific statuses and rewrite the response\n\n\n\n### Default configuration\n\n```json\n{\n \"ranges\" : [ {\n \"from\" : 500,\n \"to\" : 599\n } ],\n \"templates\" : {\n \"default\" : \"\\n \\n

      An error occurred with id: ${error_id}

      \\n

      please contact your administrator with this error id !

      \\n \\n\"\n },\n \"log\" : true,\n \"export\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoEndpoint }\n\n## Geolocation endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoEndpoint`\n\n### Description\n\nThis plugin will expose current geolocation informations on the following endpoint `/.well-known/otoroshi/plugins/geolocation`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgGeolocationInfoHeader }\n\n## Geolocation header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgGeolocationInfoHeader`\n\n### Description\n\nThis plugin will send informations extracted by the Geolocation details extractor to the target service in a header.\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasAllowedUsersValidator }\n\n## Allowed users only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasAllowedUsersValidator`\n\n### Description\n\nThis plugin only let allowed users pass\n\n\n\n### Default configuration\n\n```json\n{\n \"usernames\" : [ ],\n \"emails\" : [ ],\n \"email_domains\" : [ ],\n \"metadata_match\" : [ ],\n \"metadata_not_match\" : [ ],\n \"profile_match\" : [ ],\n \"profile_not_match\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator }\n\n## Client Certificate + Api Key only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingApikeyValidator`\n\n### Description\n\nCheck if a client certificate is present in the request and that the apikey used matches the client certificate.\nYou can set the client cert. DN in an apikey metadata named `allowed-client-cert-dn`\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator }\n\n## Client certificate matching (over http)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingHttpValidator`\n\n### Description\n\nCheck if client certificate matches the following fetched from an http endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertMatchingValidator }\n\n## Client certificate matching\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertMatchingValidator`\n\n### Description\n\nCheck if client certificate matches the following configuration\n\n\n\n### Default configuration\n\n```json\n{\n \"serial_numbers\" : [ ],\n \"subject_dns\" : [ ],\n \"issuer_dns\" : [ ],\n \"regex_subject_dns\" : [ ],\n \"regex_issuer_dns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHasClientCertValidator }\n\n## Client Certificate Only\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHasClientCertValidator`\n\n### Description\n\nCheck if a client certificate is present in the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHtmlPatcher }\n\n## Html Patcher\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHtmlPatcher`\n\n### Description\n\nThis plugin can inject elements in html pages (in the body or in the head) returned by the service\n\n\n\n### Default configuration\n\n```json\n{\n \"append_head\" : [ ],\n \"append_body\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgHttpClientCache }\n\n## HTTP Client Cache\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgHttpClientCache`\n\n### Description\n\nThis plugin add cache headers to responses\n\n\n\n### Default configuration\n\n```json\n{\n \"max_age_seconds\" : 86400,\n \"methods\" : [ \"GET\" ],\n \"status\" : [ 200 ],\n \"mime_types\" : [ \"text/html\" ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor }\n\n## Geolocation details extractor (using IpStack api)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIpStackGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [IpStack dbs](https://ipstack.com/).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"apikey\" : null,\n \"timeout\" : 2000,\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Canary }\n\n## Izanami V1 Canary Campaign\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Canary`\n\n### Description\n\nThis plugin allow you to perform canary testing based on an izanami experiment campaign (A/B test)\n\n\n\n### Default configuration\n\n```json\n{\n \"experiment_id\" : \"foo:bar:qix\",\n \"config_id\" : \"foo:bar:qix:config\",\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 5000,\n \"route_config\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgIzanamiV1Proxy }\n\n## Izanami v1 APIs Proxy\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgIzanamiV1Proxy`\n\n### Description\n\nThis plugin exposes routes to proxy Izanami configuration and features tree APIs\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"/api/izanami\",\n \"feature_pattern\" : \"*\",\n \"config_pattern\" : \"*\",\n \"auto_context\" : false,\n \"features_enabled\" : true,\n \"features_with_context_enabled\" : true,\n \"configuration_enabled\" : false,\n \"tls\" : {\n \"certs\" : [ ],\n \"trusted_certs\" : [ ],\n \"enabled\" : false,\n \"loose\" : false,\n \"trust_all\" : false\n },\n \"izanami_url\" : \"https://izanami.foo.bar\",\n \"client_id\" : \"client\",\n \"client_secret\" : \"secret\",\n \"timeout\" : 500\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgJwtUserExtractor }\n\n## Jwt user extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgJwtUserExtractor`\n\n### Description\n\nThis plugin extract a user from a JWT token\n\n\n\n### Default configuration\n\n```json\n{\n \"verifier\" : \"none\",\n \"strict\" : true,\n \"strip\" : false,\n \"name_path\" : null,\n \"email_path\" : null,\n \"meta_path\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyApikeyCall }\n\n## Legacy apikeys\n\n### Defined on steps\n\n - `MatchRoute`\n - `ValidateAccess`\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyApikeyCall`\n\n### Description\n\nThis plugin expects to find an apikey to allow the request to pass. This plugin behaves exactly like the service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"extractors\" : {\n \"basic\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"custom_headers\" : {\n \"enabled\" : true,\n \"client_id_header_name\" : null,\n \"client_secret_header_name\" : null\n },\n \"client_id\" : {\n \"enabled\" : true,\n \"header_name\" : null,\n \"query_name\" : null\n },\n \"jwt\" : {\n \"enabled\" : true,\n \"secret_signed\" : true,\n \"keypair_signed\" : true,\n \"include_request_attrs\" : false,\n \"max_jwt_lifespan_sec\" : null,\n \"header_name\" : null,\n \"query_name\" : null,\n \"cookie_name\" : null\n }\n },\n \"routing\" : {\n \"enabled\" : false\n },\n \"validate\" : true,\n \"mandatory\" : true,\n \"pass_with_user\" : false,\n \"wipe_backend_request\" : true,\n \"update_quotas\" : true\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLegacyAuthModuleCall }\n\n## Legacy Authentication\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLegacyAuthModuleCall`\n\n### Description\n\nThis plugin applies an authentication module the same way service descriptor does\n\n\n\n### Default configuration\n\n```json\n{\n \"public_patterns\" : [ ],\n \"private_patterns\" : [ ],\n \"pass_with_apikey\" : false,\n \"auth_module\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgLog4ShellFilter }\n\n## Log4Shell mitigation plugin\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgLog4ShellFilter`\n\n### Description\n\nThis plugin try to detect Log4Shell attacks in request and block them\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"body\" : \"\",\n \"parse_body\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor }\n\n## Geolocation details extractor (using Maxmind db)\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgMaxMindGeolocationInfoExtractor`\n\n### Description\n\nThis plugin extract geolocation informations from ip address using the [Maxmind dbs](https://www.maxmind.com/en/geoip2-databases).\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"path\" : \"global\",\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgResponseCache }\n\n## Response Cache\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgResponseCache`\n\n### Description\n\nThis plugin can cache responses from target services in the otoroshi datasstore\nIt also provides a debug UI at `/.well-known/otoroshi/bodylogger`.\n\n\n\n### Default configuration\n\n```json\n{\n \"ttl\" : 3600000,\n \"maxSize\" : 52428800,\n \"autoClean\" : true,\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgSecurityTxt }\n\n## Security Txt\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgSecurityTxt`\n\n### Description\n\nThis plugin exposes a special route `/.well-known/security.txt` as proposed at [https://securitytxt.org/](https://securitytxt.org/)\n\n\n\n### Default configuration\n\n```json\n{\n \"contact\" : \"contact@foo.bar\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgServiceQuotas }\n\n## Public quotas\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgServiceQuotas`\n\n### Description\n\nThis plugin will enforce public quotas on the current route\n\n\n\n### Default configuration\n\n```json\n{\n \"throttling_quota\" : 10000000,\n \"daily_quota\" : 10000000,\n \"monthly_quota\" : 10000000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgTrafficMirroring }\n\n## Traffic Mirroring\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgTrafficMirroring`\n\n### Description\n\nThis plugin will mirror every request to other targets\n\n\n\n### Default configuration\n\n```json\n{\n \"to\" : \"https://foo.bar.dev\",\n \"enabled\" : true,\n \"capture_response\" : false,\n \"generate_events\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentExtractor }\n\n## User-Agent details extractor\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentExtractor`\n\n### Description\n\nThis plugin extract informations from User-Agent header such as browsser version, OS version, etc.\nThe informations are store in plugins attrs for other plugins to use\n\n\n\n### Default configuration\n\n```json\n{\n \"log\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoEndpoint }\n\n## User-Agent endpoint\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoEndpoint`\n\n### Description\n\nThis plugin will expose current user-agent informations on the following endpoint: /.well-known/otoroshi/plugins/user-agent\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.NgUserAgentInfoHeader }\n\n## User-Agent header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.NgUserAgentInfoHeader`\n\n### Description\n\nThis plugin will sent informations extracted by the User-Agent details extractor to the target service in a header\n\n\n\n### Default configuration\n\n```json\n{\n \"header_name\" : \"X-User-Agent-Info\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth1Caller }\n\n## OAuth1 caller\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth1Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth1.\n Consumer key, secret, and OAuth token et OAuth token secret can be pass through the metadata of an api key\n or via the configuration of this plugin.\n\n\n\n### Default configuration\n\n```json\n{\n \"consumerKey\" : null,\n \"consumerSecret\" : null,\n \"token\" : null,\n \"tokenSecret\" : null,\n \"algo\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OAuth2Caller }\n\n## OAuth2 caller\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OAuth2Caller`\n\n### Description\n\nThis plugin can be used to call api that are authenticated using OAuth2 client_credential/password flow.\nDo not forget to enable client retry to handle token generation on expire.\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"client_credentials\",\n \"url\" : \"https://127.0.0.1:8080/oauth/token\",\n \"method\" : \"POST\",\n \"headerName\" : \"Authorization\",\n \"headerValueFormat\" : \"Bearer %s\",\n \"jsonPayload\" : false,\n \"clientId\" : \"the client_id\",\n \"clientSecret\" : \"the client_secret\",\n \"scope\" : null,\n \"audience\" : null,\n \"user\" : null,\n \"password\" : null,\n \"cacheTokenSeconds\" : 600000,\n \"tlsConfig\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenAsApikey }\n\n## OIDC access_token as apikey\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenAsApikey`\n\n### Description\n\nThis plugin will use the third party apikey configuration to generate an apikey\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCAccessTokenValidator }\n\n## OIDC access_token validator\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCAccessTokenValidator`\n\n### Description\n\nThis plugin will use the third party apikey configuration and apply it while keeping the apikey mecanism of otoroshi.\nUse it to combine apikey validation and OIDC access_token validation.\n\n\n\n### Default configuration\n\n```json\n{\n \"enabled\" : true,\n \"atLeastOne\" : false,\n \"config\" : {\n \"enabled\" : true,\n \"quotasEnabled\" : true,\n \"uniqueApiKey\" : false,\n \"type\" : \"OIDC\",\n \"oidcConfigRef\" : \"some-oidc-auth-module-id\",\n \"localVerificationOnly\" : false,\n \"mode\" : \"Tmp\",\n \"ttl\" : 0,\n \"headerName\" : \"Authorization\",\n \"throttlingQuota\" : 100,\n \"dailyQuota\" : 10000000,\n \"monthlyQuota\" : 10000000,\n \"excludedPatterns\" : [ ],\n \"scopes\" : [ ],\n \"rolesPath\" : [ ],\n \"roles\" : [ ]\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OIDCHeaders }\n\n## OIDC headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OIDCHeaders`\n\n### Description\n\nThis plugin injects headers containing tokens and profile from current OIDC provider.\n\n\n\n### Default configuration\n\n```json\n{\n \"profile\" : {\n \"send\" : false,\n \"headerName\" : \"X-OIDC-User\"\n },\n \"idToken\" : {\n \"send\" : false,\n \"name\" : \"id_token\",\n \"headerName\" : \"X-OIDC-Id-Token\",\n \"jwt\" : true\n },\n \"accessToken\" : {\n \"send\" : false,\n \"name\" : \"access_token\",\n \"headerName\" : \"X-OIDC-Access-Token\",\n \"jwt\" : true\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiChallenge }\n\n## Otoroshi challenge token\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiChallenge`\n\n### Description\n\nThis plugin adds a jwt challenge token to the request to a backend and expects a response with a matching token\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"V2\",\n \"ttl\" : 30,\n \"request_header_name\" : null,\n \"response_header_name\" : null,\n \"algo_to_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"algo_from_backend\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n },\n \"state_resp_leeway\" : 10\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiHeadersIn }\n\n## Otoroshi headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiHeadersIn`\n\n### Description\n\nThis plugin adds Otoroshi specific headers to the request\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OtoroshiInfos }\n\n## Otoroshi info. token\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OtoroshiInfos`\n\n### Description\n\nThis plugin adds a jwt token with informations about the caller to the backend\n\n\n\n### Default configuration\n\n```json\n{\n \"version\" : \"Latest\",\n \"ttl\" : 30,\n \"header_name\" : null,\n \"add_fields\" : null,\n \"algo\" : {\n \"type\" : \"HSAlgoSettings\",\n \"size\" : 512,\n \"secret\" : \"secret\",\n \"base64\" : false\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.OverrideHost }\n\n## Override host header\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.OverrideHost`\n\n### Description\n\nThis plugin override the current Host header with the Host of the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.PublicPrivatePaths }\n\n## Public/Private paths\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.PublicPrivatePaths`\n\n### Description\n\nThis plugin allows or forbid request based on path patterns\n\n\n\n### Default configuration\n\n```json\n{\n \"strict\" : false,\n \"private_patterns\" : [ ],\n \"public_patterns\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.QueryTransformer }\n\n## Query param transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.QueryTransformer`\n\n### Description\n\nThis plugin can modify the query params of the request\n\n\n\n### Default configuration\n\n```json\n{\n \"remove\" : [ ],\n \"rename\" : { },\n \"add\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RBAC }\n\n## RBAC\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RBAC`\n\n### Description\n\nThis plugin check if current user/apikey/jwt token has the right role\n\n\n\n### Default configuration\n\n```json\n{\n \"allow\" : [ ],\n \"deny\" : [ ],\n \"allow_all\" : false,\n \"deny_all\" : false,\n \"jwt_path\" : null,\n \"apikey_path\" : null,\n \"user_path\" : null,\n \"role_prefix\" : null,\n \"roles\" : \"roles\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ReadOnlyCalls }\n\n## Read only requests\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ReadOnlyCalls`\n\n### Description\n\nThis plugin verifies the current request only reads data\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Redirection }\n\n## Redirection\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Redirection`\n\n### Description\n\nThis plugin redirects the current request elsewhere\n\n\n\n### Default configuration\n\n```json\n{\n \"code\" : 303,\n \"to\" : \"https://www.otoroshi.io\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersIn }\n\n## Remove headers in\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersIn`\n\n### Description\n\nThis plugin removes headers in the incoming otoroshi request\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RemoveHeadersOut }\n\n## Remove headers out\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RemoveHeadersOut`\n\n### Description\n\nThis plugin removes headers in the otoroshi response\n\n\n\n### Default configuration\n\n```json\n{\n \"header_names\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.Robots }\n\n## Robots\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.Robots`\n\n### Description\n\nThis plugin provides all the necessary tool to handle search engine robots\n\n\n\n### Default configuration\n\n```json\n{\n \"robot_txt_enabled\" : true,\n \"robot_txt_content\" : \"User-agent: *\\nDisallow: /\\n\",\n \"meta_enabled\" : true,\n \"meta_content\" : \"noindex,nofollow,noarchive\",\n \"header_enabled\" : true,\n \"header_content\" : \"noindex, nofollow, noarchive\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.RoutingRestrictions }\n\n## Routing Restrictions\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.RoutingRestrictions`\n\n### Description\n\nThis plugin apply routing restriction `method domain/path` on the current request/route\n\n\n\n### Default configuration\n\n```json\n{\n \"allow_last\" : true,\n \"allowed\" : [ ],\n \"forbidden\" : [ ],\n \"not_found\" : [ ]\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.S3Backend }\n\n## S3 Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.S3Backend`\n\n### Description\n\nThis plugin is able to S3 bucket with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"bucket\" : \"\",\n \"endpoint\" : \"\",\n \"region\" : \"eu-west-1\",\n \"access\" : \"client\",\n \"secret\" : \"secret\",\n \"key\" : \"\",\n \"chunkSize\" : 8388608,\n \"v4auth\" : true,\n \"writeEvery\" : 60000,\n \"acl\" : \"private\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SOAPAction }\n\n## SOAP action\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SOAPAction`\n\n### Description\n\nThis plugin is able to call SOAP actions and expose it as a rest endpoint\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : null,\n \"envelope\" : \"\",\n \"action\" : null,\n \"preserve_query\" : true,\n \"charset\" : null,\n \"jq_request_filter\" : null,\n \"jq_response_filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SendOtoroshiHeadersBack }\n\n## Send otoroshi headers back\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SendOtoroshiHeadersBack`\n\n### Description\n\nThis plugin adds response header containing useful informations about the current call\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.SnowMonkeyChaos }\n\n## Snow Monkey Chaos\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.SnowMonkeyChaos`\n\n### Description\n\nThis plugin introduce some chaos into you life\n\n\n\n### Default configuration\n\n```json\n{\n \"large_request_fault\" : null,\n \"large_response_fault\" : null,\n \"latency_injection_fault\" : null,\n \"bad_responses_fault\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticBackend }\n\n## Static backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticBackend`\n\n### Description\n\nThis plugin is able to serve a static folder with file content\n\n\n\n### Default configuration\n\n```json\n{\n \"root_path\" : \"/tmp\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.StaticResponse }\n\n## Static Response\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.StaticResponse`\n\n### Description\n\nThis plugin returns static responses\n\n\n\n### Default configuration\n\n```json\n{\n \"status\" : 200,\n \"headers\" : { },\n \"body\" : \"\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TailscaleSelectTargetByName }\n\n## Tailscale select target by name\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TailscaleSelectTargetByName`\n\n### Description\n\nThis plugin selects a machine instance on Tailscale network based on its name\n\n\n\n### Default configuration\n\n```json\n{\n \"machine_name\" : \"my-machine\",\n \"use_ip_address\" : false\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.TcpTunnel }\n\n## TCP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.TcpTunnel`\n\n### Description\n\nThis plugin creates TCP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.UdpTunnel }\n\n## UDP Tunnel\n\n### Defined on steps\n\n - `HandlesTunnel`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.UdpTunnel`\n\n### Description\n\nThis plugin creates UDP tunnels through otoroshi\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.W3CTracing }\n\n## W3C Trace Context\n\n### Defined on steps\n\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.W3CTracing`\n\n### Description\n\nThis plugin propagates W3C Trace Context spans and can export it to Jaeger or Zipkin\n\n\n\n### Default configuration\n\n```json\n{\n \"kind\" : \"noop\",\n \"endpoint\" : \"http://localhost:3333/spans\",\n \"timeout\" : 30000,\n \"baggage\" : { }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmAccessValidator }\n\n## Wasm Access control\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmAccessValidator`\n\n### Description\n\nDelegate route access to a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmBackend }\n\n## Wasm Backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmBackend`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as backend\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmOPA }\n\n## Open Policy Agent (OPA)\n\n### Defined on steps\n\n - `ValidateAccess`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmOPA`\n\n### Description\n\nRepo policies as WASM modules\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : true,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmPreRoute }\n\n## Wasm pre-route\n\n### Defined on steps\n\n - `PreRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmPreRoute`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as in pre-route phase\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRequestTransformer }\n\n## Wasm Request Transformer\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRequestTransformer`\n\n### Description\n\nTransform the content of the request with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmResponseTransformer }\n\n## Wasm Response Transformer\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmResponseTransformer`\n\n### Description\n\nTransform the content of a response with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouteMatcher }\n\n## Wasm Route Matcher\n\n### Defined on steps\n\n - `MatchRoute`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouteMatcher`\n\n### Description\n\nThis plugin can be used to use a wasm plugin as route matcher\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmRouter }\n\n## Wasm Router\n\n### Defined on steps\n\n - `Router`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmRouter`\n\n### Description\n\nCan decide for routing with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.WasmSink }\n\n## Wasm Sink\n\n### Defined on steps\n\n - `Sink`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.WasmSink`\n\n### Description\n\nHandle unmatched requests with a wasm plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"source\" : {\n \"kind\" : \"Unknown\",\n \"path\" : \"\",\n \"opts\" : { }\n },\n \"memoryPages\" : 20,\n \"functionName\" : null,\n \"config\" : { },\n \"allowedHosts\" : [ ],\n \"allowedPaths\" : { },\n \"wasi\" : false,\n \"opa\" : false,\n \"authorizations\" : {\n \"httpAccess\" : false,\n \"proxyHttpCallTimeout\" : 5000,\n \"globalDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginDataStoreAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"globalMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"pluginMapAccess\" : {\n \"read\" : false,\n \"write\" : false\n },\n \"proxyStateAccess\" : false,\n \"configurationAccess\" : false\n },\n \"instances\" : 1,\n \"killOptions\" : {\n \"immortal\" : false,\n \"max_calls\" : 2147483647,\n \"max_memory_usage\" : 0,\n \"max_avg_call_duration\" : 0,\n \"max_unused_duration\" : 300000\n }\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XForwardedHeaders }\n\n## X-Forwarded-* headers\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XForwardedHeaders`\n\n### Description\n\nThis plugin adds all the X-Forwarded-* headers to the request for the backend target\n\n\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonRequest }\n\n## request body xml-to-json\n\n### Defined on steps\n\n - `TransformRequest`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonRequest`\n\n### Description\n\nThis plugin transform incoming request body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.XmlToJsonResponse }\n\n## response body xml-to-json\n\n### Defined on steps\n\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.XmlToJsonResponse`\n\n### Description\n\nThis plugin transform response body from xml to json and may apply a jq transformation\n\n\n\n### Default configuration\n\n```json\n{\n \"filter\" : null\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.plugins.ZipFileBackend }\n\n## Zip file backend\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.plugins.ZipFileBackend`\n\n### Description\n\nServes content from a zip file\n\n\n\n### Default configuration\n\n```json\n{\n \"url\" : \"https://github.com/MAIF/otoroshi/releases/download/16.11.2/otoroshi-manual-16.11.2.zip\",\n \"headers\" : { },\n \"dir\" : \"./zips\",\n \"prefix\" : null,\n \"ttl\" : 3600000\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.next.tunnel.TunnelPlugin }\n\n## Remote tunnel calls\n\n### Defined on steps\n\n - `CallBackend`\n\n### Plugin reference\n\n`cp:otoroshi.next.tunnel.TunnelPlugin`\n\n### Description\n\nThis plugin can contact remote service using tunnels\n\n\n\n### Default configuration\n\n```json\n{\n \"tunnel_id\" : \"default\"\n}\n```\n\n\n\n\n\n@@@\n\n\n@@@ div { .ng-plugin .plugin-hidden .pl #otoroshi.wasm.proxywasm.NgCorazaWAF }\n\n## Coraza WAF\n\n### Defined on steps\n\n - `ValidateAccess`\n - `TransformRequest`\n - `TransformResponse`\n\n### Plugin reference\n\n`cp:otoroshi.wasm.proxywasm.NgCorazaWAF`\n\n### Description\n\nCoraza WAF plugin\n\n\n\n### Default configuration\n\n```json\n{\n \"ref\" : \"none\"\n}\n```\n\n\n\n\n\n@@@\n\n"},{"name":"create-plugins.md","id":"/plugins/create-plugins.md","url":"/plugins/create-plugins.html","title":"Create plugins","content":"# Create plugins\n\n@@@ warning\nThis section is under rewrite. The following content is deprecated\n@@@\n\nWhen everything has failed and you absolutely need a feature in Otoroshi to make your use case work, there is a solution. Plugins is the feature in Otoroshi that allow you to code how Otoroshi should behave when receiving, validating and routing an http request. With request plugin, you can change request / response headers and request / response body the way you want, provide your own apikey, etc.\n\n## Plugin types\n\nthere are many plugin types explained @ref:[here](./plugins.md) \n\n## Code and signatures\n\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/requestsink.scala#L14-L19\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/routing.scala#L75-L78\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/accessvalidator.scala#L65-L85\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/script.scala#269-L540\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/eventlistener.scala#L27-L48\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L69-L164\n* https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/script/job.scala#L108-L110\n\n\nfor more information about APIs you can use\n\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#package\n* https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.mvc.Results\n* https://github.com/MAIF/otoroshi\n* https://doc.akka.io/docs/akka/2.5/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/index.html\n* https://doc.akka.io/api/akka/current/akka/stream/scaladsl/Source.html\n\n## Plugin examples\n\n@ref:[A lot of plugins](./built-in-plugins.md) comes with otoroshi, you can find them on [github](https://github.com/MAIF/otoroshi/tree/master/otoroshi/app/plugins)\n\n## Writing a plugin from Otoroshi UI\n\nLog into Otoroshi and go to `Settings (cog icon) / Plugins`. Here you can create multiple request transformer scripts and associate it with service descriptor later.\n\n@@@ div { .centered-img }\n\n@@@\n\nwhen you write for instance a transformer in the Otoroshi UI, do the following\n\n```scala\nimport akka.stream.Materializer\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass MyTransformer extends RequestTransformer {\n\n val logger = Logger(\"my-transformer\")\n\n // implements the methods you want\n}\n\n// WARN: do not forget this line to provide a working instance of your transformer to Otoroshi\nnew MyTransformer()\n```\n\nYou can use the compile button to check if the script compiles, or code the transformer in your IDE (see next point).\n\nThen go to a service descriptor, scroll to the bottom of the page, and select your transformer in the list\n\n@@@ div { .centered-img }\n\n@@@\n\n## Providing a transformer from Java classpath\n\nYou can write your own transformer using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.\n\n```scala\nlazy val root = (project in file(\".\")).\n settings(\n inThisBuild(List(\n organization := \"com.example\",\n scalaVersion := \"2.12.7\",\n version := \"0.1.0-SNAPSHOT\"\n )),\n name := \"request-transformer-example\",\n libraryDependencies += \"fr.maif\" %% \"otoroshi\" % \"1.x.x\"\n )\n```\n\n@@@ warning\nyou MUST provide plugins that lies in the `otoroshi_plugins` package or in a sub-package of `otoroshi_plugins`. If you do not, your plugin will not be found by otoroshi. for example\n\n```scala\npackage otoroshi_plugins.com.my.company.myplugin\n```\n\nalso you don't have to instantiate your plugin at the end of the file like in the Otoroshi UI\n@@@\n\nWhen your code is ready, create a jar file \n\n```\nsbt package\n```\n\nand add the jar file to the Otoroshi classpath\n\n```sh\njava -cp \"/path/to/transformer.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nthen, in your service descriptor, you can chose your transformer in the list. If you want to do it from the API, you have to defined the transformerRef using `cp:` prefix like \n\n```json\n{\n \"transformerRef\": \"cp:otoroshi_plugins.my.class.package.MyTransformer\"\n}\n```\n\n## Getting custom configuration from the Otoroshi config. file\n\nLet say you need to provide custom configuration values for a script, then you can customize a configuration file of Otoroshi\n\n```hocon\ninclude \"application.conf\"\n\notoroshi {\n scripts {\n enabled = true\n }\n}\n\nmy-transformer {\n env = \"prod\"\n maxRequestBodySize = 2048\n maxResponseBodySize = 2048\n}\n```\n\nthen start Otoroshi like\n\n```sh\njava -Dconfig.file=/path/to/custom.conf -jar otoroshi.jar\n```\n\nthen, in your transformer, you can write something like \n\n```scala\npackage otoroshi_plugins.com.example.otoroshi\n\nimport akka.stream.Materializer\nimport akka.stream.scaladsl._\nimport akka.util.ByteString\nimport env.Env\nimport models.{ApiKey, PrivateAppsUser, ServiceDescriptor}\nimport otoroshi.script._\nimport play.api.Logger\nimport play.api.mvc.{Result, Results}\nimport scala.util._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass BodyLengthLimiter extends RequestTransformer {\n\n override def def transformResponseWithCtx(ctx: TransformerResponseContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxResponseBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n\n override def transformRequestWithCtx(ctx: TransformerRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {\n val max = env.configuration.getOptional[Long](\"my-transformer.maxRequestBodySize\").getOrElse(Long.MaxValue)\n ctx.body.limitWeighted(max)(_.size)\n }\n}\n```\n\n## Using a library that is not embedded in Otoroshi\n\nJust use the `classpath` option when running Otoroshi\n\n```sh\njava -cp \"/path/to/library.jar:$/path/to/otoroshi.jar\" play.core.server.ProdServerStart\n```\n\nBe carefull as your library can conflict with other libraries used by Otoroshi and affect its stability\n\n## Enabling plugins\n\nplugins can be enabled per service from the service settings page or globally from the danger zone in the plugins section.\n\n## Full example\n\na full external plugin example can be found @link:[here](https://github.com/mathieuancelin/otoroshi-wasmer-plugin)\n"},{"name":"index.md","id":"/plugins/index.md","url":"/plugins/index.html","title":"Otoroshi plugins","content":"# Otoroshi plugins\n\nIn this sections, you will find informations about Otoroshi plugins system\n\n* @ref:[Plugins system](./plugins.md)\n* @ref:[Create plugins](./create-plugins.md)\n* @ref:[Built in plugins](./built-in-plugins.md)\n* @ref:[Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@ index\n\n* [Plugins system](./plugins.md)\n* [Create plugins](./create-plugins.md)\n* [Built in plugins](./built-in-plugins.md)\n* [Built in legacy plugins](./built-in-legacy-plugins.md)\n\n@@@"},{"name":"plugins.md","id":"/plugins/plugins.md","url":"/plugins/plugins.html","title":"Otoroshi plugins system","content":"# Otoroshi plugins system\n\nOtoroshi includes several extension points that allows you to create your own plugins and support stuff not supported by default.\n\n## Kind of available plugins\n\n@@@ div { .plugin-kind }\n###Request Sink\n\nUsed when no services are matched in Otoroshi. Can reply with any content.\n@@@\n\n@@@ div { .plugin-kind }\n###Pre routing\n\nUsed to extract values (like custom apikeys) and provide them to other plugins or Otoroshi engine\n@@@\n\n@@@ div { .plugin-kind }\n###Access Validator\n\nUsed to validate if a request can pass or not based on whatever you want\n@@@\n\n@@@ div { .plugin-kind }\n###Request Transformer\n\nUsed to transform request, responses and their body. Can be used to return arbitrary content\n@@@\n\n@@@ div { .plugin-kind }\n###Event listener\n\nAny plugin type can listen to Otoroshi internal events and react to thems\n@@@\n\n@@@ div { .plugin-kind }\n###Job\n\nTasks that can run automatically once, on be scheduled with a cron expression or every defined interval\n@@@\n\n@@@ div { .plugin-kind }\n###Exporter\n\nUsed to export events and Otoroshi alerts to an external source\n@@@\n\n@@@ div { .plugin-kind }\n###Request handler\n\nUsed to handle traffic without passing through Otoroshi routing and apply own rules\n@@@\n\n@@@ div { .plugin-kind }\n###Nano app\n\nUsed to write an api directly in Otoroshi in Scala language\n@@@"},{"name":"search.md","id":"/search.md","url":"/search.html","title":"Search otoroshi documentation","content":"# Search otoroshi documentation\n\n
      \n\n"},{"name":"anonymous-reporting.md","id":"/topics/anonymous-reporting.md","url":"/topics/anonymous-reporting.html","title":"Anonymous reporting","content":"# Anonymous reporting\n\nThe best way of supporting us in Otoroshi developement is to enable Anonymous reporting.\n\n## Details\n\nWhen this feature is active, Otoroshi perdiodically send anonymous information about its configuration.\n\nThis information helps us to know how Otoroshi is used, it's a precious hint to prioritise our roadmap.\n\nBelow is an example of what is send by Otoroshi. You can find more information about these fields either on @ref:[entities documentation](../entities/index.md) or [by reading the source code](https://github.com/MAIF/otoroshi/blob/master/otoroshi/app/jobs/reporting.scala#L174-L458).\n\n```json\n{\n \"@timestamp\": 1679514537259,\n \"timestamp_str\": \"2023-03-22T20:48:57.259+01:00\",\n \"@id\": \"4edb54171-8156-4947-b821-41d6c2bd1ba7\",\n \"otoroshi_cluster_id\": \"1148aee35-a487-47b0-b494-a2a44862c618\",\n \"otoroshi_version\": \"16.0.0-dev\",\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"datastore\": \"file\",\n \"env\": \"dev\",\n \"features\": {\n \"snow_monkey\": false,\n \"clever_cloud\": false,\n \"kubernetes\": false,\n \"elastic_read\": true,\n \"lets_encrypt\": false,\n \"auto_certs\": false,\n \"wasm_manager\": true,\n \"backoffice_login\": false\n },\n \"stats\": {\n \"calls\": 3823,\n \"data_in\": 480406,\n \"data_out\": 4698261,\n \"rate\": 0,\n \"duration\": 35.89899494949495,\n \"overhead\": 24.696984848484846,\n \"data_in_rate\": 0,\n \"data_out_rate\": 0,\n \"concurrent_requests\": 0\n },\n \"engine\": {\n \"uses_new\": true,\n \"uses_new_full\": false\n },\n \"cluster\": {\n \"mode\": \"Leader\",\n \"all_nodes\": 1,\n \"alive_nodes\": 1,\n \"leaders_count\": 1,\n \"workers_count\": 0,\n \"nodes\": [\n {\n \"id\": \"node_15ac62ec3-3e0d-48c1-a8ea-15de97088e3c\",\n \"os\": {\n \"name\": \"Mac OS X\",\n \"version\": \"13.1\",\n \"arch\": \"x86_64\"\n },\n \"java_version\": {\n \"version\": \"11.0.16.1\",\n \"vendor\": \"Eclipse Adoptium\"\n },\n \"version\": \"16.0.0-dev\",\n \"type\": \"Leader\",\n \"cpu_usage\": 10.992902320605205,\n \"load_average\": 44.38720703125,\n \"heap_used\": 527,\n \"heap_size\": 2048,\n \"relay\": true,\n \"tunnels\": 0\n }\n ]\n },\n \"entities\": {\n \"scripts\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"routes\": {\n \"count\": 24,\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"router_routes\": {\n \"count\": 27,\n \"http_clients\": {\n \"ahc\": 25,\n \"akka\": 2,\n \"netty\": 0,\n \"akka_ws\": 0\n },\n \"plugins\": {\n \"min\": 1,\n \"max\": 26,\n \"avg\": 4\n }\n },\n \"route_compositions\": {\n \"count\": 1,\n \"plugins\": {\n \"min\": 1,\n \"max\": 1,\n \"avg\": 1\n },\n \"by_kind\": {\n \"global\": 1\n }\n },\n \"apikeys\": {\n \"count\": 6,\n \"by_kind\": {\n \"disabled\": 0,\n \"with_rotation\": 0,\n \"with_read_only\": 0,\n \"with_client_id_only\": 0,\n \"with_constrained_services\": 0,\n \"with_meta\": 2,\n \"with_tags\": 1\n },\n \"authorized_on\": {\n \"min\": 1,\n \"max\": 4,\n \"avg\": 2\n }\n },\n \"jwt_verifiers\": {\n \"count\": 6,\n \"by_strategy\": {\n \"pass_through\": 6\n },\n \"by_alg\": {\n \"HSAlgoSettings\": 6\n }\n },\n \"certificates\": {\n \"count\": 9,\n \"by_kind\": {\n \"auto_renew\": 6,\n \"exposed\": 6,\n \"client\": 1,\n \"keypair\": 1\n }\n },\n \"auth_modules\": {\n \"count\": 8,\n \"by_kind\": {\n \"basic\": 7,\n \"oauth2\": 1\n }\n },\n \"service_descriptors\": {\n \"count\": 3,\n \"plugins\": {\n \"old\": 0,\n \"new\": 0\n },\n \"by_kind\": {\n \"disabled\": 1,\n \"fault_injection\": 0,\n \"health_check\": 1,\n \"gzip\": 0,\n \"jwt\": 0,\n \"cors\": 1,\n \"auth\": 0,\n \"protocol\": 0,\n \"restrictions\": 0\n }\n },\n \"teams\": {\n \"count\": 2\n },\n \"tenants\": {\n \"count\": 2\n },\n \"service_groups\": {\n \"count\": 2\n },\n \"data_exporters\": {\n \"count\": 10,\n \"by_kind\": {\n \"elastic\": 5,\n \"file\": 2,\n \"metrics\": 1,\n \"console\": 1,\n \"s3\": 1\n }\n },\n \"otoroshi_admins\": {\n \"count\": 5,\n \"by_kind\": {\n \"simple\": 2,\n \"webauthn\": 3\n }\n },\n \"backoffice_sessions\": {\n \"count\": 1,\n \"by_kind\": {\n \"simple\": 1\n }\n },\n \"private_apps_sessions\": {\n \"count\": 0,\n \"by_kind\": {}\n },\n \"tcp_services\": {\n \"count\": 0\n }\n },\n \"plugins_usage\": {\n \"cp:otoroshi.next.plugins.AdditionalHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.DisableHttp10\": 2,\n \"cp:otoroshi.next.plugins.OverrideHost\": 27,\n \"cp:otoroshi.next.plugins.TailscaleFetchCertificate\": 1,\n \"cp:otoroshi.next.plugins.OtoroshiInfos\": 6,\n \"cp:otoroshi.next.plugins.MissingHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.Redirection\": 2,\n \"cp:otoroshi.next.plugins.OtoroshiChallenge\": 5,\n \"cp:otoroshi.next.plugins.BuildMode\": 2,\n \"cp:otoroshi.next.plugins.XForwardedHeaders\": 2,\n \"cp:otoroshi.next.plugins.NgLegacyAuthModuleCall\": 2,\n \"cp:otoroshi.next.plugins.Cors\": 4,\n \"cp:otoroshi.next.plugins.OtoroshiHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.NgDefaultRequestBody\": 1,\n \"cp:otoroshi.next.plugins.NgHttpClientCache\": 1,\n \"cp:otoroshi.next.plugins.ReadOnlyCalls\": 2,\n \"cp:otoroshi.next.plugins.RemoveHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.JwtVerificationOnly\": 1,\n \"cp:otoroshi.next.plugins.ApikeyCalls\": 3,\n \"cp:otoroshi.next.plugins.WasmAccessValidator\": 3,\n \"cp:otoroshi.next.plugins.WasmBackend\": 3,\n \"cp:otoroshi.next.plugins.IpAddressAllowedList\": 2,\n \"cp:otoroshi.next.plugins.AuthModule\": 4,\n \"cp:otoroshi.next.plugins.RemoveHeadersOut\": 2,\n \"cp:otoroshi.next.plugins.IpAddressBlockList\": 2,\n \"cp:otoroshi.next.proxy.ProxyEngine\": 1,\n \"cp:otoroshi.next.plugins.JwtVerification\": 3,\n \"cp:otoroshi.next.plugins.GzipResponseCompressor\": 2,\n \"cp:otoroshi.next.plugins.SendOtoroshiHeadersBack\": 3,\n \"cp:otoroshi.next.plugins.AdditionalHeadersIn\": 4,\n \"cp:otoroshi.next.plugins.SOAPAction\": 1,\n \"cp:otoroshi.next.plugins.NgLegacyApikeyCall\": 6,\n \"cp:otoroshi.next.plugins.ForceHttpsTraffic\": 2,\n \"cp:otoroshi.next.plugins.NgErrorRewriter\": 1,\n \"cp:otoroshi.next.plugins.MissingHeadersIn\": 2,\n \"cp:otoroshi.next.plugins.MaintenanceMode\": 3,\n \"cp:otoroshi.next.plugins.RoutingRestrictions\": 2,\n \"cp:otoroshi.next.plugins.HeadersValidation\": 2\n }\n}\n```\n\n## Toggling\n\nAnonymous reporting can be toggled any time using :\n\n- the UI (Features > Danger zone > Send anonymous reports)\n- `otoroshi.anonymous-reporting.enabled` configuration\n- `OTOROSHI_ANONYMOUS_REPORTING_ENABLED` env variable\n"},{"name":"chaos-engineering.md","id":"/topics/chaos-engineering.md","url":"/topics/chaos-engineering.html","title":"Chaos engineering with the Snow Monkey","content":"# Chaos engineering with the Snow Monkey\n\nNihonzaru (the Snow Monkey) is the chaos engineering tool provided by Otoroshi. You can access it at `Settings (cog icon) / Snow Monkey`.\n\n@@@ div { .centered-img }\n\n@@@\n\n## Chaos engineering\n\nOtoroshi offers some tools to introduce [chaos engineering](https://principlesofchaos.org/) in your everyday life. With chaos engineering, you will improve the resilience of your architecture by creating faults in production on running systems. With [Nihonzaru (the snow monkey)](https://en.wikipedia.org/wiki/Japanese_macaque) Otoroshi helps you to create faults on http request/response handled by Otoroshi. \n\n@@@ div { .centered-img }\n\n@@@\n\n## Settings\n\n@@@ div { .centered-img }\n\n@@@\n\nThe snow monkey let you define a few settings to work properly :\n\n* **Include user facing apps.**: you want to create fault in production, but maybe you don't want your users to enjoy some nice snow monkey generated error pages. Using this switch let you include of not user facing apps (ui apps). Each service descriptor has a `User facing app switch` that will be used by the snow monkey.\n* **Dry run**: when dry run is enabled, outages will be registered and will generate events and alerts (in the otoroshi eventing system) but requests won't be actualy impacted. It's a good way to prepare applications to the snow monkey habits\n* **Outage strategy**: Either `AllServicesPerGroup` or `OneServicePerGroup`. It means that only one service per group or all services per groups will have `n` outages (see next bullet point) during the snow monkey working period\n* **Outages per day**: during snow monkey working period, each service per group or one service per group will have only `n` outages registered \n* **Working period**: the snow monkey only works during a working period. Here you can defined when it starts and when it stops\n* **Outage duration**: here you can defined the bounds for the random outage duration when an outage is created on a service\n* **Impacted groups**: here you can define a list of service groups impacted by the snow monkey. If none is specified, then all service groups will be impacted\n\n## Faults\n\nWith the snow monkey, you can generate four types of faults\n\n* **Large request fault**: Add trailing bytes at the end of the request body (if one)\n* **Large response fault**: Add trailing bytes at the end of the response body\n* **Latency injection fault**: Add random response latency between two bounds\n* **Bad response injection fault**: Create predefined responses with custom headers, body and status code\n\nEach fault let you define a ratio for impacted requests. If you specify a ratio of `0.2`, then 20% of the requests for the impacte service will be impacted by this fault\n\n@@@ div { .centered-img }\n\n@@@\n\nThen you juste have to start the snow monkey and enjoy the show ;)\n\n@@@ div { .centered-img }\n\n@@@\n\n## Current outages\n\nIn the last section of the snow monkey page, you can see current outages (per service), when they started, their duration, etc ...\n\n@@@ div { .centered-img }\n\n@@@"},{"name":"dev-portal.md","id":"/topics/dev-portal.md","url":"/topics/dev-portal.html","title":"Developer portal with Daikoku","content":"# Developer portal with Daikoku\n\nWhile Otoroshi is the perfect tool to manage your webapps in a technical point of view it lacked of business perspective. This is not the case anymore with Daikoku.\n\nWhile Otoroshi is a standalone, Daikoku is a developer portal which stands in front of Otoroshi and provides some business feature.\n\nWhether you want to use Daikoku for your public APIs, you want to monetize or with your private APIs to provide some documentation, facilitation and self-service feature, it will be the perfect portal for Otoroshi.\n\n@@@div { .plugin .platform }\n## Daikoku\n\nRun your first Daikoku with a simple jar or with one Docker command.\n\n\n
      \nTry Daikoku \n
      \n@link:[With jar](https://maif.github.io/daikoku/devmanual/getdaikoku/frombinaries.html)\n@link:[With Docker](https://maif.github.io/daikoku/devmanual/getdaikoku/fromdocker.html)\n@@@\n\n@@@div { .plugin .platform }\n## Contribute\n\nDaikoku is opensource, so all contributions are welcome.\n\n\n@link:[Show the repository](https://github.com/MAIF/daikoku)\n@@@\n\n@@@div { .plugin .platform }\n## Documentation\n\nDaikoku and its UI are fully documented.\n\n\n@link:[Read the documentation](https://maif.github.io/daikoku/devmanual/)\n@@@\n\n"},{"name":"engine.md","id":"/topics/engine.md","url":"/topics/engine.html","title":"Proxy engine","content":"# Proxy engine\n\nStarting from the `1.5.3` release, otoroshi offers a new plugin that implements the next generation of the proxy engine. \nThis engine has been designed based on our 5 years experience building, maintaining and running the previous one.\nIt tries to fix all the drawback we may have encountered during those years and highly improve performances, user experience, reporting and debugging capabilities. \n\nThe new engine is fully plugin oriented in order to spend CPU cycles only on useful stuff. You can enable this plugin only on some domain names so you can easily A/B test the new engine. The new proxy engine is designed to be more reactive and more efficient generally. It is also designed to be very efficient on path routing where it wasn't the old engines strong suit.\n\nStarting from version `16.0.0`, this engine will be enabled by default on any new otoroshi cluster. In a future version, the engine will be enabled for any new or exisiting otoroshi cluster.\n\n## Enabling the new engine\n\nBy default, all freshly started Otoroshi instances have the new proxy engine enabled by default, for the other, to enable the new proxy engine on an otoroshi instance, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration, enable it and in `domains` add the values of the desired domains (let say we want to use the new engine on `api.foo.bar`. It is possible to use `*.foo.bar` if that's what you want to do).\n\nThe next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the previous one.\n\n```json\n{\n \"NextGenProxyEngine\" : {\n \"enabled\" : true,\n \"debug_headers\" : false,\n \"reporting\": true,\n \"domains\" : [ \"api.foo.bar\" ],\n \"deny_domains\" : [ ],\n }\n}\n```\n\nif you need to enable global plugin with the new engine, you can add the following configuration in the `global plugins` configuration object \n\n```javascript\n{\n ...\n \"ng\": {\n \"slots\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.W3CTracing\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"baggage\": {\n \"foo\": \"bar\"\n }\n }\n },\n {\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.RequestSinkWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.apikeys.ClientCredentialService\",\n \"ClientCredentialService\": {\n \"domain\": \"ccs-next-gen.oto.tools\",\n \"expiration\": 3600000,\n \"defaultKeyPair\": \"otoroshi-jwt-signing\",\n \"secure\": false\n }\n }\n }\n ]\n }\n ...\n}\n```\n\n## Entities\n\nThis plugin introduces new entities that will replace (one day maybe) service descriptors:\n\n - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins\n - `backends`: a list of targets to contact a backend\n\n## Entities sync\n\nA new behavior introduced for the new proxy engine is the entities sync job. To avoid unecessary operations on the underlying datastore when routing requests, a new job has been setup in otoroshi that synchronize the content of the datastore (at least a part of it) with an in-memory cache. Because of it, the propagation of changes between an admin api call and the actual result on routing can be longer than before. When a node creates, updates, or deletes an entity via the admin api, other nodes need to wait for the next poll to purge the old cached entity and start using the new one. You can change the interval between syncs with the configuration key `otoroshi.next.state-sync-interval` or the env. variable `OTOROSHI_NEXT_STATE_SYNC_INTERVAL`. The default value is `10000` and the unit is `milliseconds`\n\n@@@ warning\nBecause of entities sync, memory consumption of otoroshi will be significantly higher than previous versions. You can use `otoroshi.next.monitor-proxy-state-size=true` config (or `OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE` env. variable) to monitor the actual memory size of the entities cache. This will produce the `ng-proxy-state-size-monitoring` metric in standard otoroshi metrics\n@@@\n\n## Automatic conversion\n\nThe new engine uses new entities for its configuration, but in order to facilitate transition between the old world and the new world, all the `service descriptors` of an otoroshi instance are automatically converted live into `routes` periodically. Any `service descriptor` should still work as expected through the new engine while enjoying all the perks.\n\n@@@ warning\nthe experimental nature of the engine can imply unexpected behaviors for converted service descriptors\n@@@\n\n## Routing\n\nthe new proxy engine introduces a new router that has enhanced capabilities and performances. The router can handle thousands of routes declarations without compromising performances.\n\nThe new route allow routes to be matched on a combination of\n\n* hostname\n* path\n* header values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n* query param values\n * where values can be `exact_value`, or `Regex(value_regex)`, or `Wildcard(value_with_*)`\n\npatch matching works \n\n* exactly\n * matches `/api/foo` with `/api/foo` and not with `/api/foo/bar`\n* starting with value (default behavior, like the previous engine)\n * matches `/api/foo` with `/api/foo` but also with `/api/foo/bar`\n\npath matching can also include wildcard paths and even path params\n\n* plain old path: `subdomain.domain.tld/api/users`\n* wildcard path: `subdomain.domain.tld/api/users/*/bills`\n* named path params: `subdomain.domain.tld/api/users/:id/bills`\n* named regex path params: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n\nhostname matching works on \n\n* exact values\n * `subdomain.domain.tld`\n* wildcard values like\n * `*.domain.tld`\n * `subdomain.*.tld`\n\nas path matching can now include named path params, it is possible to perform a ful url rewrite on the target path like \n\n* input: `subdomain.domain.tld/api/users/$id<[0-9]+>/bills`\n* output: `target.domain.tld/apis/v1/basic_users/${req.pathparams.id}/all_bills`\n\n## Plugins\n\nthe new route entity defines a plugin pipline where any plugin can be enabled or not and can be active only on some paths. \nEach plugin slot in the pipeline holds the plugin id and the plugin configuration. \n\nYou can also enable debugging only on a plugin instance instead of the whole route (see [the debugging section](#debugging))\n\n```javascript\n{ \n ...\n \"plugins\" : [ {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.OverrideHost\",\n \"include\" : [ ],\n \"exclude\" : [ ],\n \"config\" : { }\n }, {\n \"enabled\" : true,\n \"debug\" : false,\n \"plugin\" : \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"include\" : [ ],\n \"exclude\" : [ \"/openapi.json\" ],\n \"config\" : { }\n } ]\n}\n```\n\nyou can find the list of built-in plugins @ref:[here](../plugins/built-in-plugins.md)\n\n## Using legacy plugins\n\nif you need to use legacy otoroshi plugins with the new engine, you can use several wrappers in order to do so\n\n* `otoroshi.next.plugins.wrappers.PreRoutingWrapper`\n* `otoroshi.next.plugins.wrappers.AccessValidatorWrapper`\n* `otoroshi.next.plugins.wrappers.RequestSinkWrapper`\n* `otoroshi.next.plugins.wrappers.RequestTransformerWrapper`\n* `otoroshi.next.plugins.wrappers.CompositeWrapper`\n\nto use it, just declare a plugin slot with the right wrapper and in the config, declare the `plugin` you want to use and its configuration like:\n\n```javascript\n{\n \"plugin\": \"cp:otoroshi.next.plugins.wrappers.PreRoutingWrapper\",\n \"enabled\": true,\n \"include\": [],\n \"exclude\": [],\n \"config\": {\n \"plugin\": \"cp:otoroshi.plugins.jwt.JwtUserExtractor\",\n \"JwtUserExtractor\": {\n \"verifier\" : \"$ref\",\n \"strict\" : true,\n \"namePath\" : \"name\",\n \"emailPath\": \"email\",\n \"metaPath\" : null\n }\n }\n}\n```\n\n## Reporting\n\nby default, any request hiting the new engine will generate an execution report with informations about how the request pipeline steps were performed. It is possible to export those reports as `RequestFlowReport` events using classical data exporter. By default, exporting for reports is not enabled, you must enable the `export_reporting` flag on a `route` or `service`.\n\n```javascript\n{\n \"@id\": \"8efac472-07bc-4a80-8d27-4236309d7d01\",\n \"@timestamp\": \"2022-02-15T09:51:25.402+01:00\",\n \"@type\": \"RequestFlowReport\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"service_548f13bb-a809-4b1d-9008-fae3b1851092\",\n \"@service\": \"demo-service\",\n \"@env\": \"prod\",\n \"route\": {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"report\": {\n \"id\" : \"ab73707b3-946b-4853-92d4-4c38bbaac6d6\",\n \"creation\" : \"2022-02-15T09:51:25.402+01:00\",\n \"termination\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 5,\n \"duration_ns\" : 5905522,\n \"overhead\" : 4,\n \"overhead_ns\" : 4223215,\n \"overhead_in\" : 2,\n \"overhead_in_ns\" : 2687750,\n \"overhead_out\" : 1,\n \"overhead_out_ns\" : 1535465,\n \"state\" : \"Successful\",\n \"steps\" : [ {\n \"task\" : \"start-handling\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 177430,\n \"ctx\" : null\n }, {\n \"task\" : \"check-concurrent-requests\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085402,\n \"stop_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 145242,\n \"ctx\" : null\n }, {\n \"task\" : \"find-route\",\n \"start\" : 1644915085402,\n \"start_fmt\" : \"2022-02-15T09:51:25.402+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 497119,\n \"ctx\" : {\n \"found_route\" : {\n \"_loc\" : {\n \"tenant\" : \"default\",\n \"teams\" : [ \"default\" ]\n },\n \"id\" : \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\",\n \"name\" : \"hey\",\n \"description\" : \"hey\",\n \"tags\" : [ \"env:prod\" ],\n \"metadata\" : { },\n \"enabled\" : true,\n \"debug_flow\" : true,\n \"export_reporting\" : false,\n \"groups\" : [ \"default\" ],\n \"frontend\" : {\n \"domains\" : [ \"hey-next-gen.oto.tools/\", \"hey.oto.tools/\" ],\n \"strip_path\" : true,\n \"exact\" : false,\n \"headers\" : { },\n \"methods\" : [ ]\n },\n \"backend\" : {\n \"targets\" : [ {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n } ],\n \"target_refs\" : [ ],\n \"root\" : \"/\",\n \"rewrite\" : false,\n \"load_balancing\" : {\n \"type\" : \"RoundRobin\"\n },\n \"client\" : {\n \"useCircuitBreaker\" : true,\n \"retries\" : 1,\n \"maxErrors\" : 20,\n \"retryInitialDelay\" : 50,\n \"backoffFactor\" : 2,\n \"callTimeout\" : 30000,\n \"callAndStreamTimeout\" : 120000,\n \"connectionTimeout\" : 10000,\n \"idleTimeout\" : 60000,\n \"globalTimeout\" : 30000,\n \"sampleInterval\" : 2000,\n \"proxy\" : { },\n \"customTimeouts\" : [ ],\n \"cacheConnectionSettings\" : {\n \"enabled\" : false,\n \"queueSize\" : 2048\n }\n }\n },\n \"backend_ref\" : null,\n \"plugins\" : [ ]\n },\n \"matched_path\" : \"\",\n \"exact\" : true,\n \"params\" : { },\n \"matched_routes\" : [ \"service_dev_d54f11d0-18e2-4da4-9316-cf47733fd29a\" ]\n }\n }, {\n \"task\" : \"compute-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 105151,\n \"ctx\" : {\n \"disabled_plugins\" : [ ],\n \"filtered_plugins\" : [ ]\n }\n }, {\n \"task\" : \"tenant-check\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 26097,\n \"ctx\" : null\n }, {\n \"task\" : \"check-global-maintenance\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 14132,\n \"ctx\" : null\n }, {\n \"task\" : \"call-before-request-callbacks\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 56671,\n \"ctx\" : null\n }, {\n \"task\" : \"extract-tracking-id\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 5207,\n \"ctx\" : null\n }, {\n \"task\" : \"call-pre-route-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 39786,\n \"ctx\" : null\n }, {\n \"task\" : \"call-access-validator-plugins\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085403,\n \"stop_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 25311,\n \"ctx\" : null\n }, {\n \"task\" : \"enforce-global-limits\",\n \"start\" : 1644915085403,\n \"start_fmt\" : \"2022-02-15T09:51:25.403+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 296617,\n \"ctx\" : {\n \"remaining_quotas\" : {\n \"authorizedCallsPerSec\" : 10000000,\n \"currentCallsPerSec\" : 10000000,\n \"remainingCallsPerSec\" : 10000000,\n \"authorizedCallsPerDay\" : 10000000,\n \"currentCallsPerDay\" : 10000000,\n \"remainingCallsPerDay\" : 10000000,\n \"authorizedCallsPerMonth\" : 10000000,\n \"currentCallsPerMonth\" : 10000000,\n \"remainingCallsPerMonth\" : 10000000\n }\n }\n }, {\n \"task\" : \"choose-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 368899,\n \"ctx\" : {\n \"backend\" : {\n \"id\" : \"127.0.0.1:8081\",\n \"hostname\" : \"127.0.0.1\",\n \"port\" : 8081,\n \"tls\" : false,\n \"weight\" : 1,\n \"protocol\" : \"HTTP/1.1\",\n \"ip_address\" : null,\n \"tls_config\" : {\n \"certs\" : [ ],\n \"trustedCerts\" : [ ],\n \"mtls\" : false,\n \"loose\" : false,\n \"trustAll\" : false\n }\n }\n }\n }, {\n \"task\" : \"transform-request\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085404,\n \"stop_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 506363,\n \"ctx\" : null\n }, {\n \"task\" : \"call-backend\",\n \"start\" : 1644915085404,\n \"start_fmt\" : \"2022-02-15T09:51:25.404+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 2,\n \"duration_ns\" : 2163470,\n \"ctx\" : null\n }, {\n \"task\" : \"transform-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 279887,\n \"ctx\" : null\n }, {\n \"task\" : \"stream-response\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085407,\n \"stop_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 382952,\n \"ctx\" : null\n }, {\n \"task\" : \"trigger-analytics\",\n \"start\" : 1644915085407,\n \"start_fmt\" : \"2022-02-15T09:51:25.407+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 812036,\n \"ctx\" : null\n }, {\n \"task\" : \"request-success\",\n \"start\" : 1644915085408,\n \"start_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"stop\" : 1644915085408,\n \"stop_fmt\" : \"2022-02-15T09:51:25.408+01:00\",\n \"duration\" : 0,\n \"duration_ns\" : 0,\n \"ctx\" : null\n } ]\n }\n}\n```\n\n## Debugging\n\nwith the new reporting capabilities, the new engine also have debugging capabilities built in. In you enable the `debug_flow` flag on a route (or service), the resulting `RequestFlowReport` will be enriched with contextual informations between each plugins of the route plugin pipeline\n\n@@@ note\nyou can also use the `Try it` feature of the new route designer UI to get debug reports automatically for a specific call\n@@@\n\n## HTTP traffic capture\n\nusing the `capture` flag, a `TrafficCaptureEvent` is generated for each http request/response. This event will contains request and response body. Those events can be exported using @ref:[data exporters](../entities/data-exporters.md) as usual. You can also use the @ref:[GoReplay file exporter](../entities/data-exporters.md#goreplay-file) that is specifically designed to ingest those events and create [GoReplay](https://goreplay.org/) files (`.gor`)\n\n@@@ warning\nthis feature can have actual impact on CPU and RAM consumption\n@@@\n\n```json\n{\n \"@id\": \"d5998b0c4-cb08-43e6-9921-27472c7a56e0\",\n \"@timestamp\": 1651828801115,\n \"@type\": \"TrafficCaptureEvent\",\n \"@product\": \"otoroshi\",\n \"@serviceId\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"@service\": \"test-server\",\n \"@env\": \"prod\",\n \"route\": {\n \"id\": \"route_2b2670879-131c-423d-b755-470c7b1c74b1\",\n \"name\": \"test-server\"\n },\n \"request\": {\n \"id\": \"152250645825034725600000\",\n \"int_id\": 115,\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"test-server-next-gen.oto.tools:9999\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\",\n \"Remote-Address\": \"127.0.0.1:57660\",\n \"Timeout-Access\": \"\",\n \"Raw-Request-URI\": \"/\",\n \"Tls-Session-Info\": \"Session(1651828041285|SSL_NULL_WITH_NULL_NULL)\"\n },\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"path\": \"/\",\n \"domain\": null,\n \"http_only\": true,\n \"max_age\": null,\n \"secure\": false,\n \"same_site\": null\n }\n ],\n \"tls\": false,\n \"uri\": \"/\",\n \"path\": \"/\",\n \"version\": \"HTTP/1.1\",\n \"has_body\": true,\n \"remote\": \"127.0.0.1\",\n \"client_cert_chain\": null,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_request\": {\n \"url\": \"http://localhost:3000/\",\n \"method\": \"POST\",\n \"headers\": {\n \"Host\": \"localhost\",\n \"Accept\": \"*/*\",\n \"Cookie\": \"fifoo=fibar\",\n \"User-Agent\": \"curl/7.64.1\",\n \"Content-Type\": \"application/json\",\n \"Content-Length\": \"13\"\n },\n \"version\": \"HTTP/1.1\",\n \"client_cert_chain\": null,\n \"cookies\": [\n {\n \"name\": \"fifoo\",\n \"value\": \"fibar\",\n \"domain\": null,\n \"path\": \"/\",\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": true\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"int_id\": 33,\n \"body\": \"{\\\"foo\\\":\\\"bar\\\"}\"\n },\n \"backend_response\": {\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"id\": \"152260631569472064900000\",\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"response\": {\n \"id\": \"152250645825034725600000\",\n \"status\": 200,\n \"headers\": {\n \"Date\": \"Fri, 06 May 2022 09:20:01 GMT\",\n \"Connection\": \"keep-alive\",\n \"Set-Cookie\": \"foo=bar\",\n \"Content-Type\": \"application/json\",\n \"Transfer-Encoding\": \"chunked\"\n },\n \"cookies\": [\n {\n \"name\": \"foo\",\n \"value\": \"bar\",\n \"domain\": null,\n \"path\": null,\n \"maxAge\": null,\n \"secure\": false,\n \"httpOnly\": false\n }\n ],\n \"status_txt\": \"OK\",\n \"http_version\": \"HTTP/1.1\",\n \"body\": \"{\\\"headers\\\":{\\\"host\\\":\\\"localhost\\\",\\\"accept\\\":\\\"*/*\\\",\\\"user-agent\\\":\\\"curl/7.64.1\\\",\\\"content-type\\\":\\\"application/json\\\",\\\"cookie\\\":\\\"fifoo=fibar\\\",\\\"content-length\\\":\\\"13\\\"},\\\"method\\\":\\\"POST\\\",\\\"path\\\":\\\"/\\\",\\\"body\\\":\\\"{\\\\\\\"foo\\\\\\\":\\\\\\\"bar\\\\\\\"}\\\"}\"\n },\n \"user-agent-details\": null,\n \"origin-details\": null,\n \"instance-number\": 0,\n \"instance-name\": \"dev\",\n \"instance-zone\": \"local\",\n \"instance-region\": \"local\",\n \"instance-dc\": \"local\",\n \"instance-provider\": \"local\",\n \"instance-rack\": \"local\",\n \"cluster-mode\": \"Leader\",\n \"cluster-name\": \"otoroshi-leader-9hnv5HUXpbCZD7Ee\"\n}\n```\n\n## openapi import\n\nas the new router offers possibility to match exactly on a single path and a single method, and with the help of the `service` entity, it is now pretty easy to import openapi document as `route-compositions` entities. To do that, a new api has been made available to perform the translation. Be aware that this api **DOES NOT** save the entity and just return the result of the translation. \n\n```sh\ncurl -X POST \\\n -H 'Content-Type: application/json' \\\n -u admin-api-apikey-id:admin-api-apikey-secret \\\n 'http://otoroshi-api.oto.tools:8080/api/route-compositions/_openapi' \\\n -d '{\"domain\":\"oto-api-proxy.oto.tools\",\"openapi\":\"https://raw.githubusercontent.com/MAIF/otoroshi/master/otoroshi/public/openapi.json\"}'\n```\n\n@@@ div { .centered-img }\n\n@@@\n\n"},{"name":"events-and-analytics.md","id":"/topics/events-and-analytics.md","url":"/topics/events-and-analytics.html","title":"Events and analytics","content":"# Events and analytics\n\nOtoroshi is a solution fully traced : calls to services, access to UI, creation of resources, etc.\n\n@@@ warning\nYou have to use [Elastic](https://www.elastic.co) to enable analytics features in Otoroshi\n@@@\n\n## Events\n\n* Analytics event\n* Gateway event\n* TCP event\n* Healthcheck event\n\n## Event log\n\nOtoroshi can read his own exported events from an Elasticsearch instance, set up in the danger zone. Theses events are available from the UI, at the following route: `https://xxxxx/bo/dashboard/events`.\n\nThe `Global events` page display all events of **GatewayEvent** type. This page is a way to quickly read an interval of events and can be used in addition of a Kibana instance.\n\nFor each event, a list of information will be displayed and an additional button `content` to watch the full content of the event, at the JSON format. \n\n## Alerts \n\n* `MaxConcurrentRequestReachedAlert`: happening when the handled requests number are greater than the limit of concurrent requests indicated in the global configuration of Otoroshi\n* `CircuitBreakerOpenedAlert`: happening when the circuit breaker pass from closed to opened\n* `CircuitBreakerClosedAlert`: happening when the circuit breaker pass from opened to closed\n* `SessionDiscardedAlert`: send when an admin discarded an admin sessions\n* `SessionsDiscardedAlert`: send when an admin discarded all admin sessions\n* `PanicModeAlert`: send when panic mode is enabled\n* `OtoroshiExportAlert`: send when otoroshi global configuration is exported\n* `U2FAdminDeletedAlert`: send when an admin has deleted an other admin user\n* `BlackListedBackOfficeUserAlert`: send when a blacklisted user has tried to acccess to the UI\n* `AdminLoggedInAlert`: send when an user admin has logged to the UI\n* `AdminFirstLogin`: send when an user admin has successfully logged to the UI for the first time\n* `AdminLoggedOutAlert`: send when an user admin has logged out from Otoroshi\n* `GlobalConfigModification`: send when an user amdin has changed the global configuration of Otoroshi\n* `RevokedApiKeyUsageAlert`: send when an user admin has revoked an apikey\n* `ServiceGroupCreatedAlert`: send when an user admin has created a service group\n* `ServiceGroupUpdatedAlert`: send when an user admin has updated a service group\n* `ServiceGroupDeletedAlert`: send when an user admin has deleted a service group\n* `ServiceCreatedAlert`: send when an user admin has created a tcp service\n* `ServiceUpdatedAlert`: send when an user admin has updated a tcp service\n* `ServiceDeletedAlert`: send when an user admin has deleted a tcp service\n* `ApiKeyCreatedAlert`: send when an user admin has crated a new apikey\n* `ApiKeyUpdatedAlert`: send when an user admin has updated a new apikey\n* `ApiKeyDeletedAlert`: send when an user admin has deleted a new apikey\n\n## Audit\n\nWith Otoroshi, any admin action and any sucpicious/alert action is recorded. These records are stored in Otoroshi’s datastore (only the last n records, defined by the `otoroshi.events.maxSize` config key). All the records can be send through the analytics mechanism (WebHook, Kafka, Elastic) for external and/or further usage. We recommand sending away those records for security reasons.\n\nOtoroshi keep the following list of information for each executed action:\n\n* `Date`: moment of the action\n* `User`: name of the owner\n* `From`: IP of the concerned user\n* `Action`: action performed by the person. The possible actions are:\n\n * `ACCESS_APIKEY`: User accessed a apikey\n * `ACCESS_ALL_APIKEYS`: User accessed all apikeys\n * `CREATE_APIKEY`: User created a apikey\n * `UPDATE_APIKEY`: User updated a apikey\n * `DELETE_APIKEY`: User deleted a apikey\n * `ACCESS_AUTH_MODULE`: User accessed an Auth. module\n * `ACCESS_ALL_AUTH_MODULES`: User accessed all Auth. modules\n * `CREATE_AUTH_MODULE`: User created an Auth. module\n * `UPDATE_AUTH_MODULE`: User updated an Auth. module\n * `DELETE_AUTH_MODULE`: User deleted an Auth. module\n * `ACCESS_CERTIFICATE`: User accessed a certificate\n * `ACCESS_ALL_CERTIFICATES`: User accessed all certificates\n * `CREATE_CERTIFICATE`: User created a certificate\n * `UPDATE_CERTIFICATE`: User updated a certificate\n * `DELETE_CERTIFICATE`: User deleted a certificate\n * `ACCESS_CLIENT_CERT_VALIDATOR`: User accessed a client cert. validator\n * `ACCESS_ALL_CLIENT_CERT_VALIDATORS`: User accessed all client cert. validators\n * `CREATE_CLIENT_CERT_VALIDATOR`: User created a client cert. validator\n * `UPDATE_CLIENT_CERT_VALIDATOR`: User updated a client cert. validator\n * `DELETE_CLIENT_CERT_VALIDATOR`: User deleted a client cert. validator\n * `ACCESS_DATA_EXPORTER_CONFIG`: User accessed a data exporter config\n * `ACCESS_ALL_DATA_EXPORTER_CONFIG`: User accessed all data exporter config\n * `CREATE_DATA_EXPORTER_CONFIG`: User created a data exporter config\n * `UPDATE_DATA_EXPORTER_CONFIG`: User updated a data exporter config\n * `DELETE_DATA_EXPORTER_CONFIG`: User deleted a data exporter config\n * `ACCESS_GLOBAL_JWT_VERIFIER`: User accessed a global jwt verifier\n * `ACCESS_ALL_GLOBAL_JWT_VERIFIERS`: User accessed all global jwt verifiers\n * `CREATE_GLOBAL_JWT_VERIFIER`: User created a global jwt verifier\n * `UPDATE_GLOBAL_JWT_VERIFIER`: User updated a global jwt verifier\n * `DELETE_GLOBAL_JWT_VERIFIER`: User deleted a global jwt verifier\n * `ACCESS_SCRIPT`: User accessed a script\n * `ACCESS_ALL_SCRIPTS`: User accessed all scripts\n * `CREATE_SCRIPT`: User created a script\n * `UPDATE_SCRIPT`: User updated a script\n * `DELETE_SCRIPT`: User deleted a Script\n * `ACCESS_SERVICES_GROUP`: User accessed a service group\n * `ACCESS_ALL_SERVICES_GROUPS`: User accessed all services groups\n * `CREATE_SERVICE_GROUP`: User created a service group\n * `UPDATE_SERVICE_GROUP`: User updated a service group\n * `DELETE_SERVICE_GROUP`: User deleted a service group\n * `ACCESS_SERVICES_FROM_SERVICES_GROUP`: User accessed all services from a services group\n * `ACCESS_TCP_SERVICE`: User accessed a tcp service\n * `ACCESS_ALL_TCP_SERVICES`: User accessed all tcp services\n * `CREATE_TCP_SERVICE`: User created a tcp service\n * `UPDATE_TCP_SERVICE`: User updated a tcp service\n * `DELETE_TCP_SERVICE`: User deleted a tcp service\n * `ACCESS_TEAM`: User accessed a Team\n * `ACCESS_ALL_TEAMS`: User accessed all teams\n * `CREATE_TEAM`: User created a team\n * `UPDATE_TEAM`: User updated a team\n * `DELETE_TEAM`: User deleted a team\n * `ACCESS_TENANT`: User accessed a Tenant\n * `ACCESS_ALL_TENANTS`: User accessed all tenants\n * `CREATE_TENANT`: User created a tenant\n * `UPDATE_TENANT`: User updated a tenant\n * `DELETE_TENANT`: User deleted a tenant\n * `SERVICESEARCH`: User searched for a service\n * `ACTIVATE_PANIC_MODE`: Admin activated panic mode\n\n\n* `Message`: explicit message about the action (example: the `SERVICESEARCH` action happened when an `user searched for a service`)\n* `Content`: all information at JSON format\n\n## Global metrics\n\nThe global metrics are displayed on the index page of the Otoroshi UI. Otoroshi provides information about :\n\n* the number of requests served\n* the amount of data received and sended\n* the number of concurrent requests\n* the number of requests per second\n* the current overhead\n\nMore metrics can be found on the **Global analytics** page (available at https://xxxxxx/bo/dashboard/stats).\n\n## Monitoring services\n\nOnce you have declared services, you can monitor them with Otoroshi. \n\nLet's starting by setup Otoroshi to push events to an elastic cluster via a data exporter. Then you will can setup Otoroshi events read from an elastic cluster. Go to `settings (cog icon) / Danger Zone` and expand the `Analytics: Elastic cluster (read)` section.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service healthcheck\n\nIf you have defined an health check URL in the service descriptor, you can access the health check page from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service live stats\n\nYou can also monitor live stats like total of served request, average response time, average overhead, etc. The live stats page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n### Service analytics\n\nYou can also get some aggregated metrics. The analytics page can be accessed from the sidebar of the service page.\n\n@@@ div { .centered-img }\n\n@@@\n\n## New proxy engine\n\n### Debug reporting\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `debug_flow` flag, events of type `RequestFlowReport` are generated\n\n### Traffic capture\n\nwhen using the @ref:[new proxy engine](./engine.md), when a route or the global config. enables traffic capture using the `capture` flag, events of type `TrafficCaptureEvent` are generated. It contains everything that compose otoroshi input http request and output http responses\n"},{"name":"expression-language.md","id":"/topics/expression-language.md","url":"/topics/expression-language.html","title":"Expression language","content":"# Expression language\n\n\n\n- [Documentation and examples](#documentation-and-examples)\n- [Test the expression language](#test-the-expression-language)\n\nThe expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using @ref:[JWT verifiers](../entities/jwt-verifiers.md)). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.\n\n## Documentation and examples\n\n@@@div { #expressions }\n \n@@@\n\nIf an input contains a string starting by `${`, Otoroshi will try to evaluate the content. If the content doesn't match a known expression,\nthe 'bad-expr' value will be set.\n\n## Test the expression language\n\nYou can test to get the same values than the right part by creating these following services. \n\n```sh\n# Let's start by downloading the latest Otoroshi.\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.0-dev/otoroshi.jar'\n\n# Once downloading, run Otoroshi.\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n\n# Create an authentication module to protect the following route.\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/auths \\\n-H \"Otoroshi-Client-Id: admin-api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: admin-api-apikey-secret\" \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\"type\":\"basic\",\"id\":\"auth_mod_in_memory_auth\",\"name\":\"in-memory-auth\",\"desc\":\"in-memory-auth\",\"users\":[{\"name\":\"User Otoroshi\",\"password\":\"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i\",\"email\":\"user@foo.bar\",\"metadata\":{\"username\":\"roger\"},\"tags\":[\"foo\"],\"webauthn\":null,\"rights\":[{\"tenant\":\"*:r\",\"teams\":[\"*:r\"]}]}],\"sessionCookieValues\":{\"httpOnly\":true,\"secure\":false}}\nEOF\n\n\n# Create a proxy of the mirror.otoroshi.io on http://api.oto.tools:8080\ncurl -X POST http://otoroshi-api.oto.tools:8080/api/routes \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-H 'Content-Type: application/json; charset=utf-8' \\\n-d @- <<'EOF'\n{\n \"id\": \"expression-language-api-service\",\n \"name\": \"expression-language\",\n \"enabled\": true,\n \"frontend\": {\n \"domains\": [\n \"api.oto.tools/\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ]\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\"\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.ApikeyCalls\",\n \"config\": {\n \"validate\": true,\n \"mandatory\": true,\n \"pass_with_user\": true,\n \"wipe_backend_request\": true,\n \"update_quotas\": true\n },\n \"plugin_index\": {\n \"validate_access\": 1,\n \"transform_request\": 2,\n \"match_route\": 0\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n \"config\": {\n \"pass_with_apikey\": true,\n \"auth_module\": null,\n \"module\": \"auth_mod_in_memory_auth\"\n },\n \"plugin_index\": {\n \"validate_access\": 1\n }\n },\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.AdditionalHeadersIn\",\n \"config\": {\n \"headers\": {\n \"my-expr-header.apikey.unknown-tag\": \"${apikey.tags['0':'no-found-tag']}\",\n \"my-expr-header.request.uri\": \"${req.uri}\",\n \"my-expr-header.ctx.replace-field-all-value\": \"${ctx.foo.replaceAll('o','a')}\",\n \"my-expr-header.env.unknown-field\": \"${env.java_h:not-found-java_h}\",\n \"my-expr-header.service-id\": \"${service.id}\",\n \"my-expr-header.ctx.unknown-fields\": \"${ctx.foob|ctx.foot:not-found}\",\n \"my-expr-header.apikey.metadata\": \"${apikey.metadata.foo}\",\n \"my-expr-header.request.protocol\": \"${req.protocol}\",\n \"my-expr-header.service-domain\": \"${service.domain}\",\n \"my-expr-header.token.unknown-foo-field\": \"${token.foob:not-found-foob}\",\n \"my-expr-header.service-unknown-group\": \"${service.groups['0':'unkown group']}\",\n \"my-expr-header.env.path\": \"${env.PATH}\",\n \"my-expr-header.request.unknown-header\": \"${req.headers.foob:default value}\",\n \"my-expr-header.service-name\": \"${service.name}\",\n \"my-expr-header.token.foo-field\": \"${token.foob|token.foo}\",\n \"my-expr-header.request.path\": \"${req.path}\",\n \"my-expr-header.ctx.geolocation\": \"${ctx.geolocation.foo}\",\n \"my-expr-header.token.unknown-fields\": \"${token.foob|token.foob2:not-found}\",\n \"my-expr-header.request.unknown-query\": \"${req.query.foob:default value}\",\n \"my-expr-header.service-subdomain\": \"${service.subdomain}\",\n \"my-expr-header.date\": \"${date}\",\n \"my-expr-header.ctx.replace-field-value\": \"${ctx.foo.replace('o','a')}\",\n \"my-expr-header.apikey.name\": \"${apikey.name}\",\n \"my-expr-header.request.full-url\": \"${req.fullUrl}\",\n \"my-expr-header.ctx.default-value\": \"${ctx.foob:other}\",\n \"my-expr-header.service-tld\": \"${service.tld}\",\n \"my-expr-header.service-metadata\": \"${service.metadata.foo}\",\n \"my-expr-header.ctx.useragent\": \"${ctx.useragent.foo}\",\n \"my-expr-header.service-env\": \"${service.env}\",\n \"my-expr-header.request.host\": \"${req.host}\",\n \"my-expr-header.config.unknown-port-field\": \"${config.http.ports:not-found}\",\n \"my-expr-header.request.domain\": \"${req.domain}\",\n \"my-expr-header.token.replace-header-value\": \"${token.foo.replace('o','a')}\",\n \"my-expr-header.service-group\": \"${service.groups['0']}\",\n \"my-expr-header.ctx.foo\": \"${ctx.foo}\",\n \"my-expr-header.apikey.tag\": \"${apikey.tags['0']}\",\n \"my-expr-header.service-unknown-metadata\": \"${service.metadata.test:default-value}\",\n \"my-expr-header.apikey.id\": \"${apikey.id}\",\n \"my-expr-header.request.header\": \"${req.headers.foo}\",\n \"my-expr-header.request.method\": \"${req.method}\",\n \"my-expr-header.ctx.foo-field\": \"${ctx.foob|ctx.foo}\",\n \"my-expr-header.config.port\": \"${config.http.port}\",\n \"my-expr-header.token.unknown-foo\": \"${token.foo}\",\n \"my-expr-header.date-with-format\": \"${date.format('yyy-MM-dd')}\",\n \"my-expr-header.apikey.unknown-metadata\": \"${apikey.metadata.myfield:default value}\",\n \"my-expr-header.request.query\": \"${req.query.foo}\",\n \"my-expr-header.token.replace-header-all-value\": \"${token.foo.replaceAll('o','a')}\"\n }\n }\n }\n ]\n}\nEOF\n```\n\nCreate an apikey or use the default generate apikey.\n\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \\\n-H \"Content-type: application/json\" \\\n-u admin-api-apikey-id:admin-api-apikey-secret \\\n-d @- <<'EOF'\n{\n \"clientId\": \"api-apikey-id\",\n \"clientSecret\": \"api-apikey-secret\",\n \"clientName\": \"api-apikey-name\",\n \"description\": \"api-apikey-id-description\",\n \"authorizedGroup\": \"default\",\n \"enabled\": true,\n \"throttlingQuota\": 10,\n \"dailyQuota\": 10,\n \"monthlyQuota\": 10,\n \"tags\": [\"foo\"],\n \"metadata\": {\n \"fii\": \"bar\"\n }\n}\nEOF\n```\n\nThen try to call the first service.\n\n```sh\ncurl http://api.oto.tools:8080/api/\\?foo\\=bar \\\n-H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8\" \\\n-H \"Otoroshi-Client-Id: api-apikey-id\" \\\n-H \"Otoroshi-Client-Secret: api-apikey-secret\" \\\n-H \"foo: bar\" | jq\n```\n\nThis will returns the list of the received headers by the mirror.\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.date\": \"2021-11-26T10:54:51.112+01:00\",\n \"my-expr-header.ctx.foo\": \"no-ctx-foo\",\n \"my-expr-header.env.path\": \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n \"my-expr-header.apikey.id\": \"admin-api-apikey-id\",\n \"my-expr-header.apikey.tag\": \"one-tag\",\n \"my-expr-header.service-id\": \"expression-language-api-service\",\n \"my-expr-header.apikey.name\": \"Otoroshi Backoffice ApiKey\",\n \"my-expr-header.config.port\": \"8080\",\n \"my-expr-header.request.uri\": \"/api/?foo=bar\",\n \"my-expr-header.service-env\": \"prod\",\n \"my-expr-header.service-tld\": \"oto.tools\",\n \"my-expr-header.request.host\": \"api.oto.tools:8080\",\n \"my-expr-header.request.path\": \"/api/\",\n \"my-expr-header.service-name\": \"expression-language\",\n \"my-expr-header.ctx.foo-field\": \"no-ctx-foob-foo\",\n \"my-expr-header.ctx.useragent\": \"no-ctx-useragent.foo\",\n \"my-expr-header.request.query\": \"bar\",\n \"my-expr-header.service-group\": \"default\",\n \"my-expr-header.request.domain\": \"api.oto.tools\",\n \"my-expr-header.request.header\": \"bar\",\n \"my-expr-header.request.method\": \"GET\",\n \"my-expr-header.service-domain\": \"api.oto.tools\",\n \"my-expr-header.apikey.metadata\": \"bar\",\n \"my-expr-header.ctx.geolocation\": \"no-ctx-geolocation.foo\",\n \"my-expr-header.token.foo-field\": \"no-token-foob-foo\",\n \"my-expr-header.date-with-format\": \"2021-11-26\",\n \"my-expr-header.request.full-url\": \"http://api.oto.tools:8080/api/?foo=bar\",\n \"my-expr-header.request.protocol\": \"http\",\n \"my-expr-header.service-metadata\": \"no-meta-foo\",\n \"my-expr-header.ctx.default-value\": \"other\",\n \"my-expr-header.env.unknown-field\": \"not-found-java_h\",\n \"my-expr-header.service-subdomain\": \"api\",\n \"my-expr-header.token.unknown-foo\": \"no-token-foo\",\n \"my-expr-header.apikey.unknown-tag\": \"one-tag\",\n \"my-expr-header.ctx.unknown-fields\": \"not-found\",\n \"my-expr-header.token.unknown-fields\": \"not-found\",\n \"my-expr-header.request.unknown-query\": \"default value\",\n \"my-expr-header.service-unknown-group\": \"default\",\n \"my-expr-header.request.unknown-header\": \"default value\",\n \"my-expr-header.apikey.unknown-metadata\": \"default value\",\n \"my-expr-header.ctx.replace-field-value\": \"no-ctx-foo\",\n \"my-expr-header.token.unknown-foo-field\": \"not-found-foob\",\n \"my-expr-header.service-unknown-metadata\": \"default-value\",\n \"my-expr-header.config.unknown-port-field\": \"not-found\",\n \"my-expr-header.token.replace-header-value\": \"no-token-foo\",\n \"my-expr-header.ctx.replace-field-all-value\": \"no-ctx-foo\",\n \"my-expr-header.token.replace-header-all-value\": \"no-token-foo\",\n }\n}\n```\n\nThen try the second call to the webapp. Navigate on your browser to `http://webapp.oto.tools:8080`. Continue with `user@foo.bar` as user and `password` as credential.\n\nThis should output:\n\n```json\n{\n ...\n \"headers\": {\n ...\n \"my-expr-header.user\": \"User Otoroshi\",\n \"my-expr-header.user.email\": \"user@foo.bar\",\n \"my-expr-header.user.metadata\": \"roger\",\n \"my-expr-header.user.profile-field\": \"User Otoroshi\",\n \"my-expr-header.user.unknown-metadata\": \"not-found\",\n \"my-expr-header.user.unknown-profile-field\": \"not-found\",\n }\n}\n```"},{"name":"graphql-composer.md","id":"/topics/graphql-composer.md","url":"/topics/graphql-composer.html","title":"GraphQL Composer Plugin","content":"# GraphQL Composer Plugin\n\n
      \nRoute plugins:\nGraphQL Composer\n
      \n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\n> GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.\n[Official GraphQL website](https://graphql.org/)\n\nAPIs RESTful and GraphQL development has become one of the most popular activities for companies as well as users in recent times. In fast scaling companies, the multiplication of clients can cause the number of API needs to grow at scale.\n\nOtoroshi comes with a solution to create and meet your customers' needs without constantly creating and recreating APIs: the `GraphQL composer plugin`. The GraphQL Composer is an useful plugin to build an GraphQL API from multiples differents sources. These sources can be REST apis, GraphQL api or anything that supports the HTTP protocol. In fact, the plugin can define and expose for each of your client a specific GraphQL schema, which only corresponds to the needs of the customers.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n## Tutorial\n\nLet's take an example to get a better view of this plugin. We want to build a schema with two types: \n\n* an user with a name and a password \n* an country with a name and its users.\n\nTo build this schema, we need to use three custom directives. A `directive` decorates part of a GraphQL schema or operation with additional configuration. Directives are preceded by the @ character, like so:\n\n* @ref:[rest](#directives) : to call a http rest service with dynamic path params\n* @ref:[permission](#directives) : to restrict the access to the sensitive field\n* @ref:[graphql](#directives) : to call a graphQL service by passing a url and the associated query\n\nThe final schema of our tutorial should look like this\n```graphql\ntype Country {\n name: String\n users: [User] @rest(url: \"http://localhost:5000/countries/${item.name}/users\")\n}\n\ntype User {\n name: String\n password: String @password(value: \"ADMIN\")\n}\n\ntype Query {\n users: [User] @rest(url: \"http://localhost:5000/users\", paginate: true)\n user(id: String): User @rest(url: \"http://localhost:5000/users/${params.id}\")\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com\", query: \"{ countries { name }}\", paginate: true)\n}\n```\n\nNow you know the GraphQL Composer basics and how it works, let's configure it on our project:\n\n* create a route using the new Otoroshi router describing the previous countries API\n* add the GraphQL composer plugin\n* configure the plugin with the schema\n* try to call it\n\n@@@ div { .centered-img }\n\n@@@\n\n### Setup environment\n\nFirst of all, we need to download the latest Otoroshi.\n\n```sh\ncurl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v1.5.15/otoroshi.jar'\n```\n\nNow, just run the command belows to start the Otoroshi, and look the console to see the output.\n\n```sh\njava -Dotoroshi.adminPassword=password -jar otoroshi.jar \n```\n\nNow, login to [the UI](http://otoroshi.oto.tools:8080) with \n```sh\nuser = admin@otoroshi.io\npassword = password\n```\n\n### Create our countries API\n\nFirst thing to do in any new API is of course creating a `route`. We need 4 informations which are:\n\n* name: `My countries API`\n* frontend: exposed on `countries-api.oto.tools`\n* plugins: the list of plugins with only the `GraphQL composer` plugin\n\nLet's make a request call through the Otoroshi Admin API (with the default apikey), like the example below\n```sh\ncurl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\"countries.oto.tools\"]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\"\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\n### Build the countries API \n\nLet's continue our API by patching the configuration of the GraphQL plugin with the complete schema.\n\n```sh\ncurl -X PUT 'http://otoroshi-api.oto.tools:8080/api/routes/countries-api' \\\n -d '{\n \"id\": \"countries-api\",\n \"name\": \"My countries API\",\n \"frontend\": {\n \"domains\": [\n \"countries.oto.tools\"\n ]\n },\n \"backend\": {\n \"targets\": [\n {\n \"hostname\": \"mirror.otoroshi.io\",\n \"port\": 443,\n \"tls\": true\n }\n ],\n \"load_balancing\": {\n \"type\": \"RoundRobin\"\n }\n },\n \"plugins\": [\n {\n \"enabled\": true,\n \"plugin\": \"cp:otoroshi.next.plugins.GraphQLBackend\",\n \"config\": {\n \"schema\": \"type Country {\\n name: String\\n users: [User] @rest(url: \\\"http://localhost:8181/countries/${item.name}/users\\\", headers: \\\"{}\\\")\\n}\\n\\ntype Query {\\n users: [User] @rest(url: \\\"http://localhost:8181/users\\\", paginate: true, headers: \\\"{}\\\")\\n user(id: String): User @rest(url: \\\"http://localhost:8181/users/${params.id}\\\")\\n countries: [Country] @graphql(url: \\\"https://countries.trevorblades.com\\\", query: \\\"{ countries { name }}\\\", paginate: true)\\ntype User {\\n name: String\\n password: String }\\n\"\n }\n }\n ]\n}' \\\n -H \"Content-type: application/json\" \\\n -u admin-api-apikey-id:admin-api-apikey-secret\n```\n\nThe route is created but it expects an API, exposed on the localhost:8181, to work. \n\nLet's create this simple API which returns a list of users and of countries. This should look like the following snippet.\nThe API uses express as http server.\n\n```js\nconst express = require('express')\n\nconst app = express()\n\nconst users = [\n {\n name: 'Joe',\n password: 'password'\n },\n {\n name: 'John',\n password: 'password2'\n }\n]\n\nconst countries = [\n {\n name: 'Andorra',\n users: [users[0]]\n },\n {\n name: 'United Arab Emirates',\n users: [users[1]]\n }\n]\n\napp.get('/users', (_, res) => {\n return res.json(users)\n})\n\napp.get(`/users/:name`, (req, res) => {\n res.json(users.find(u => u.name === req.params.name))\n})\n\napp.get('/countries/:id/users', (req, res) => {\n const country = countries.find(c => c.name === req.params.id)\n\n if (country) \n return res.json(country.users)\n else \n return res.json([])\n})\n\napp.listen(8181, () => {\n console.log(`Listening on 8181`)\n});\n\n```\n\nLet's try to make a first call to our countries API.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": [\n { \n \"name\":\"Andorra\",\n \"users\": [\n { \"name\":\"Joe\" }\n ]\n }\n ]\n }\n}\n```\n\nThe call graph should looks like\n\n```\n1. Calls https://countries.trevorblades.com\n2. For each country:\n - extract the field name\n - calls http://localhost:8181/countries/${country}/users to get the list of users for this country\n```\n\nYou may have noticed that we added an argument at the end of the graphql directive named `paginate`. It enabled the paging for the client accepting limit and offset parameters. These parameters are used by the plugin to filter and reduce the content.\n\nLet's make a new call that does not accept any country.\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n }\\n }\\n}\"\n}\nEOF\n```\n\nYou should see the following content in your terminal.\n\n```json\n{\n \"data\": { \n \"countries\": []\n }\n}\n```\n\nLet's move on to the next section to secure sensitive field of our API.\n\n### Basics of permissions \n\nThe permission directives has been created to protect the fields of the graphql schema. The validation process starts by create a `context` for all incoming requests, based on the list of paths defined in the permissions field of the plugin. The permissions paths can refer to the request data (url, headers, etc), user credentials (api key, etc) and informations about the matched route. Then the process can validate that the value or values are present in the `context`.\n\n@@@div { .simple-block }\n\n
      \nPermission\n\n
      \n\n*Arguments : value and unauthorized_value*\n\nThe permission directive can be used to secure a field on **one** value. The directive checks that a specific value is present in the `context`.\n\nTwo arguments are available, the first, named `value`, is required and designates the value found. The second optional value, `unauthorized_value`, can be used to indicates, in the outcoming response, the rejection message.\n\n**Example**\n```js\ntype User {\n id: String @permission(\n value: \"FOO\", \n unauthorized_value: \"You're not authorized to get this field\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nAll permissions\n\n
      \n\n*Arguments : values and unauthorized_value*\n\nThis directive is presumably the same as the previous one except that it takes a list of values.\n\n**Example**\n```js\ntype User {\n id: String @allpermissions(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO and BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nOne permissions of\n\n
      \n*Arguments : values and unauthorized_value*\n\nThis directive takes a list of values and validate that one of them is in the context.\n\n**Example**\n```js\ntype User {\n id: String @onePermissionsOf(\n values: [\"FOO\", \"BAR\"], \n unauthorized_value: \"FOO or BAR could not be found\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nAuthorize\n\n
      \n\n*Arguments : path, value and unauthorized_value*\n\nThe authorize directive has one more required argument, named `path`, which indicates the path to value, in the context. Unlike the last three directives, the authorize directive doesn't search in the entire context but at the specified path.\n\n**Example**\n```js\ntype User {\n id: String @authorize(\n path: \"$.raw_request.headers.foo\", \n value: \"BAR\", \n unauthorized_value: \"Bar could not be found in the foo header\")\n}\n```\n@@@\n\nLet's restrict the password field to the users that comes with a `role` header of the value `ADMIN`.\n\n1. Patch the configuration of the API by adding the permissions in the configuration of the plugin.\n```json\n...\n \"permissions\": [\"$.raw_request.headers.role\"]\n...\n```\n\n1. Add an directive on the password field in the schema\n```graphql\ntype User {\n name: String\n password: String @permission(value: \"ADMIN\")\n}\n```\n\nLet's make a call with the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: ADMIN'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nNow try to change the value of the role header\n\n```sh\ncurl 'countries.oto.tools:9999/' \\\n--header 'Content-Type: application/json' \\\n--header 'role: USER'\n--data-binary @- << EOF\n{\n \"query\": \"{\\n countries(limit: 0) {\\n name\\n users {\\n name\\n password\\n }\\n }\\n}\"\n}\nEOF\n```\n\nThe error message should look like \n\n```json\n{\n \"errors\": [\n {\n \"message\": \"You're not authorized\",\n \"path\": [\n \"countries\",\n 0,\n \"users\",\n 0,\n \"password\"\n ],\n ...\n }\n ]\n}\n```\n\n\n# Glossary\n\n## Directives\n\n@@@div { .simple-block }\n\n
      \nRest\n\n
      \n\n*Arguments : url, method, headers, timeout, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to expose servers that communicate using the http protocol. The only required argument is the `url`.\n\n**Example**\n```js\ntype Query {\n users(limit: Int, offset: Int): [User] @rest(url: \"http://foo.oto.tools/users\", method: \"GET\")\n}\n```\n\nIt can be placed on the field of a query and type. To custom your url queries, you can use the path parameter and another field with respectively, `params` and `item` variables.\n\n**Example**\n```js\ntype Country {\n name: String\n phone: String\n users: [User] @rest(url: \"http://foo.oto.tools/users/${item.name}\")\n}\n\ntype Query {\n user(id: String): User @rest(url: \"http://foo.oto.tools/users/${params.id}\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nGraphQL\n\n
      \n\n*Arguments : url, method, headers, timeout, query, data, response_path, response_filter, limit, offset, paginate*\n\nThe rest directive is used to call an other graphql server.\n\nThe required argument are the `url` and the `query`.\n\n**Example**\n```js\ntype Query {\n countries: [Country] @graphql(url: \"https://countries.trevorblades.com/\", query: \"{ countries { name phone }}\")\n}\n\ntype Country {\n name: String\n phone: String\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nSoap\n\n
      \n*Arguments: all following arguments*\n\nThe soap directive is used to call a soap service. \n\n```js\ntype Query {\n randomNumber: String @soap(\n jq_response_filter: \".[\\\"soap:Envelope\\\"] | .[\\\"soap:Body\\\"] | .[\\\"m:NumberToWordsResponse\\\"] | .[\\\"m:NumberToWordsResult\\\"]\", \n url: \"https://www.dataaccess.com/webservicesserver/numberconversion.wso\", \n envelope: \" \\n \\n \\n \\n 12 \\n \\n \\n\")\n}\n```\n\n\n##### Specific arguments\n\n| Argument | Type | Optional | Default value |\n| --------------------------- | --------- | -------- | ------------- |\n| envelope | *STRING* | Required | |\n| url | *STRING* | x | |\n| action | *STRING* | x | |\n| preserve_query | *BOOLEAN* | Required | true |\n| charset | *STRING* | x | |\n| convert_request_body_to_xml | *BOOLEAN* | Required | true |\n| jq_request_filter | *STRING* | x | |\n| jq_response_filter | *STRING* | x | |\n\n@@@\n\n@@@div { .simple-block }\n\n
      \nJSON\n\n
      \n*Arguments: path, json, paginate*\n\nThe json directive can be used to expose static data or mocked data. The first usage is to defined a raw stringify JSON in the `data` argument. The second usage is to set data in the predefined field of the GraphQL plugin composer and to specify a path in the `path` argument.\n\n**Example**\n```js\ntype Query {\n users_from_raw_data: [User] @json(data: \"[{\\\"firstname\\\":\\\"Foo\\\",\\\"name\\\":\\\"Bar\\\"}]\")\n users_from_predefined_data: [User] @json(path: \"users\")\n}\n```\n@@@\n\n@@@div { .simple-block }\n\n
      \nMock\n\n
      \n*Arguments: url*\n\nThe mock directive is to used with the Mock Responses Plugin, also named `Charlatan`. This directive can be interesting to mock your schema and start to use your Otoroshi route before starting to develop the underlying service.\n\n**Example**\n```js\ntype Query {\n users: @mock(url: \"/users\")\n}\n```\n\nThis example supposes that the Mock Responses plugin is set on the route's feed, and that an endpoint `/users` is available.\n\n@@@\n\n### List of directive arguments\n\n| Argument | Type | Optional | Default value |\n| ------------------ | ---------------- | --------------------------- | ------------- |\n| url | *STRING* | | |\n| method | *STRING* | x | GET |\n| headers | *STRING* | x | |\n| timeout | *INT* | x | 5000 |\n| data | *STRING* | x | |\n| path | *STRING* | x (only for json directive) | |\n| query | *STRING* | x | |\n| response_path | *STRING* | x | |\n| response_filter | *STRING* | x | |\n| limit | *INT* | x | |\n| offset | *INT* | x | |\n| value | *STRING* | | |\n| values | LIST of *STRING* | |\n| path | *STRING* | | |\n| paginate | *BOOLEAN* | x | |\n| unauthorized_value | *STRING* | x (only for permissions directive) | |\n"},{"name":"green-score.md","id":"/topics/green-score.md","url":"/topics/green-score.html","title":"Green Score","content":"# Green Score\n\nThe Green Score provide aggregated, quantitative data about the performance and behavior of an API over time. It is an aggregation of static and dynamic values that are coming from the usage of routes in Otoroshi. The main objective is to advise users on the consumption of their APIs and services.\n\n\n\nOtoroshi has a complete integration of the collective rules, divided into four concerns: **Architecture**, **Design**, **Usage** and **Logs retention**. The 6000 score points are spread over the four parts and a final note is given for each group of routes.\n\nThe API green score is available on 16.9.0 or later version of Otoroshi. You can find the feature on the search bar of your Otoroshi UI or directly in the sidebar by clicking on **Green score**.\n\nTo start the process, click on Add New Group, give a name and select a first route to audit. After clicking on the hammer icon, you can select the rules respected by your route. Before saving, you can adjust the values used to calculate the dynamic score. These thresholds are used to calculate a second green score depending on the amount of data you want not to exceed from your downstream service and the following other values: \n\n* **Overhead**: Otoroshi's calculation time to handle the request and response\n* **Duration**: the complete duration from the recpetion of the request by Otoroshi until the client gets a response\n* **Backend duration**: the time required for downstream service to respond to Otoroshi\n* **Calls**: the rate of calls by seconds\n* **Data in**: the amount of data received by the downstream service\n* **Data out**: the amount of data produced by the downstream service\n* **Headers in**: the amount of headers received by the downstream service\n* **Headers out**: the amount of headers produced by the downstream service\n\nThe Green Score works for all architectures, including simple leader or more advanced concept like [clustering](https://maif.github.io/otoroshi/manual/deploy/clustering.html)."},{"name":"http3.md","id":"/topics/http3.md","url":"/topics/http3.html","title":"HTTP3 support","content":"# HTTP3 support\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nHTTP3 server and client previews are available in otoroshi since version 1.5.14\n\n\n## Server\n\nto enable http3 server preview, you need to enable the following flags\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048\n```\n\nthen you will be able to send HTTP3 request on port 10048. For instance, using [quiche-client](https://github.com/cloudflare/quiche)\n\n```sh\ncargo run --bin quiche-client -- --no-verify 'https://my-service.oto.tools:10048'\n```\n\n## Client\n\nto consume services exposed with HTTP3, just select the `HTTP/3.0` protocol in the backend target."},{"name":"index.md","id":"/topics/index.md","url":"/topics/index.html","title":"Detailed topics","content":"# Detailed topics\n\nIn this sections, you will find informations about various Otoroshi topics \n\n* @ref:[Proxy engine](./engine.md)\n* @ref:[WASM support](./wasm-usage.md)\n* @ref:[Chaos engineering](./chaos-engineering.md)\n* @ref:[TLS](./tls.md)\n* @ref:[Otoroshi's PKI](./pki.md)\n* @ref:[Monitoring](./monitoring.md)\n* @ref:[Events and analytics](./events-and-analytics.md)\n* @ref:[Developer portal with Daikoku](./dev-portal.md)\n* @ref:[Sessions management](./sessions-mgmt.md)\n* @ref:[The Otoroshi communication protocol](./otoroshi-protocol.md)\n* @ref:[Expression language](./expression-language.md)\n* @ref:[Otoroshi user rights](./user-rights.md)\n* @ref:[GraphQL composer](./graphql-composer.md)\n* @ref:[Secret vaults](./secrets.md)\n* @ref:[Otoroshi tunnels](./tunnels.md)\n* @ref:[Relay routing](./relay-routing.md)\n* @ref:[Alternative http backend](./netty-server.md)\n* @ref:[HTTP3 support](./http3.md)\n* @ref:[Anonymous reporting](./anonymous-reporting.md)\n* @ref:[OpenTelemetry support](./opentelemetry.md)\n* @ref:[Green score](./green-score.md)\n\n@@@ index\n\n* [Proxy engine](./engine.md)\n* [WASM support](./wasm-usage.md)\n* [Chaos engineering](./chaos-engineering.md)\n* [TLS](./tls.md)\n* [Otoroshi's PKI](./pki.md)\n* [Monitoring](./monitoring.md)\n* [Events and analytics](./events-and-analytics.md)\n* [Developer portal with Daikoku](./dev-portal.md)\n* [Sessions management](./sessions-mgmt.md)\n* [The Otoroshi communication protocol](./otoroshi-protocol.md)\n* [Expression language](./expression-language.md)\n* [Otoroshi user rights](./user-rights.md)\n* [GraphQL composer](./graphql-composer.md)\n* [Secret vaults](./secrets.md)\n* [Otoroshi tunnels](./tunnels.md)\n* [Relay routing](./relay-routing.md)\n* [Alternative http backend](./netty-server.md)\n* [HTTP3 support](./http3.md)\n* [Anonymous reporting](./anonymous-reporting.md)\n* [OpenTelemetry support](./opentelemetry.md)\n* [Green score](./green-score.md)\n \n@@@\n"},{"name":"monitoring.md","id":"/topics/monitoring.md","url":"/topics/monitoring.html","title":"Monitoring","content":"# Monitoring\n\nThe Otoroshi API exposes two endpoints to know more about instance health. All the following endpoint are exposed on the instance host through it's ip address. It is also exposed on the otoroshi api hostname and the otoroshi backoffice hostname\n\n* `/health`: the health of the Otoroshi instance\n* `/metrics`: the metrics of the Otoroshi instance, either in JSON or Prometheus format using the `Accept` header (with `application/json` / `application/prometheus` values) or the `format` query param (with `json` or `prometheus` values)\n* `/live`: returns an http 200 response `{\"live\": true}` when the service is alive\n* `/ready`: return an http 200 response `{\"ready\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"ready\": false}`\n* `/startup`: return an http 200 response `{\"started\": true}` when the instance is ready to accept traffic (certs synced, plugins compiled, etc). if not, returns http 503 `{\"started\": false}`\n\nthose routes are also available on any hostname leading to otoroshi with a twist in the URL\n\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/health\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/metrics\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/live\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/ready\n* http://xxxxxxxx.xxxxx.xx/.well-known/otoroshi/monitoring/startup\n\n## Endpoints security\n\nThe two endpoints are exposed publicly on the Otoroshi admin api. But you can remove the corresponding public pattern and query the endpoints using standard apikeys. If you don't want to use apikeys but don't want to expose the endpoints publicly, you can defined two config. variables (`otoroshi.health.accessKey` or `HEALTH_ACCESS_KEY` and `otoroshi.metrics.accessKey` or `OTOROSHI_METRICS_ACCESS_KEY`) that will hold an access key for the endpoints. Then you can call the endpoints with an `access_key` query param with the value defined in the config. If you don't defined `otoroshi.metrics.accessKey` but define `otoroshi.health.accessKey`, `otoroshi.metrics.accessKey` will have the value of `otoroshi.health.accessKey`.\n \n## Examples\n\nlet say `otoroshi.health.accessKey` has value `MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY`\n\n```sh\n$ curl http://otoroshi-api.oto.tools:8080/health\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"otoroshi\":\"healthy\",\"datastore\":\"healthy\"}\n\n$ curl -H 'Accept: application/json' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n{\"version\":\"4.0.0\",\"gauges\":{\"attr.app.commit\":{\"value\":\"xxxx\"},\"attr.app.id\":{\"value\":\"xxxx\"},\"attr.cluster.mode\":{\"value\":\"Leader\"},\"attr.cluster.name\":{\"value\":\"otoroshi-leader-0\"},\"attr.instance.env\":{\"value\":\"prod\"},\"attr.instance.id\":{\"value\":\"xxxx\"},\"attr.instance.number\":{\"value\":\"0\"},\"attr.jvm.cpu.usage\":{\"value\":136},\"attr.jvm.heap.size\":{\"value\":1409},\"attr.jvm.heap.used\":{\"value\":112},\"internals.0.concurrent-requests\":{\"value\":1},\"internals.global.throttling-quotas\":{\"value\":2},\"jvm.attr.name\":{\"value\":\"2085@xxxx\"},\"jvm.attr.uptime\":{\"value\":2296900},\"jvm.attr.vendor\":{\"value\":\"JDK11\"},\"jvm.gc.PS-MarkSweep.count\":{\"value\":3},\"jvm.gc.PS-MarkSweep.time\":{\"value\":261},\"jvm.gc.PS-Scavenge.count\":{\"value\":12},\"jvm.gc.PS-Scavenge.time\":{\"value\":161},\"jvm.memory.heap.committed\":{\"value\":1477967872},\"jvm.memory.heap.init\":{\"value\":1690304512},\"jvm.memory.heap.max\":{\"value\":3005218816},\"jvm.memory.heap.usage\":{\"value\":0.03916456777568639},\"jvm.memory.heap.used\":{\"value\":117698096},\"jvm.memory.non-heap.committed\":{\"value\":166445056},\"jvm.memory.non-heap.init\":{\"value\":7667712},\"jvm.memory.non-heap.max\":{\"value\":994050048},\"jvm.memory.non-heap.usage\":{\"value\":0.1523920694986979},\"jvm.memory.non-heap.used\":{\"value\":151485344},\"jvm.memory.pools.CodeHeap-'non-nmethods'.committed\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-nmethods'.max\":{\"value\":5832704},\"jvm.memory.pools.CodeHeap-'non-nmethods'.usage\":{\"value\":0.28408093398876405},\"jvm.memory.pools.CodeHeap-'non-nmethods'.used\":{\"value\":1656960},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.committed\":{\"value\":11796480},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.usage\":{\"value\":0.09536102872567315},\"jvm.memory.pools.CodeHeap-'non-profiled-nmethods'.used\":{\"value\":11721088},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.committed\":{\"value\":37355520},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.init\":{\"value\":2555904},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.max\":{\"value\":122912768},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.usage\":{\"value\":0.2538573047187417},\"jvm.memory.pools.CodeHeap-'profiled-nmethods'.used\":{\"value\":31202304},\"jvm.memory.pools.Compressed-Class-Space.committed\":{\"value\":14942208},\"jvm.memory.pools.Compressed-Class-Space.init\":{\"value\":0},\"jvm.memory.pools.Compressed-Class-Space.max\":{\"value\":367001600},\"jvm.memory.pools.Compressed-Class-Space.usage\":{\"value\":0.033858838762555805},\"jvm.memory.pools.Compressed-Class-Space.used\":{\"value\":12426248},\"jvm.memory.pools.Metaspace.committed\":{\"value\":99794944},\"jvm.memory.pools.Metaspace.init\":{\"value\":0},\"jvm.memory.pools.Metaspace.max\":{\"value\":375390208},\"jvm.memory.pools.Metaspace.usage\":{\"value\":0.25168142904782426},\"jvm.memory.pools.Metaspace.used\":{\"value\":94478744},\"jvm.memory.pools.PS-Eden-Space.committed\":{\"value\":349700096},\"jvm.memory.pools.PS-Eden-Space.init\":{\"value\":422576128},\"jvm.memory.pools.PS-Eden-Space.max\":{\"value\":1110966272},\"jvm.memory.pools.PS-Eden-Space.usage\":{\"value\":0.07505125052077188},\"jvm.memory.pools.PS-Eden-Space.used\":{\"value\":83379408},\"jvm.memory.pools.PS-Eden-Space.used-after-gc\":{\"value\":0},\"jvm.memory.pools.PS-Old-Gen.committed\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.init\":{\"value\":1127219200},\"jvm.memory.pools.PS-Old-Gen.max\":{\"value\":2253914112},\"jvm.memory.pools.PS-Old-Gen.usage\":{\"value\":0.014950035505168354},\"jvm.memory.pools.PS-Old-Gen.used\":{\"value\":33696096},\"jvm.memory.pools.PS-Old-Gen.used-after-gc\":{\"value\":23791152},\"jvm.memory.pools.PS-Survivor-Space.committed\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.init\":{\"value\":70254592},\"jvm.memory.pools.PS-Survivor-Space.max\":{\"value\":1048576},\"jvm.memory.pools.PS-Survivor-Space.usage\":{\"value\":0.59375},\"jvm.memory.pools.PS-Survivor-Space.used\":{\"value\":622592},\"jvm.memory.pools.PS-Survivor-Space.used-after-gc\":{\"value\":622592},\"jvm.memory.total.committed\":{\"value\":1644412928},\"jvm.memory.total.init\":{\"value\":1697972224},\"jvm.memory.total.max\":{\"value\":3999268864},\"jvm.memory.total.used\":{\"value\":269184904},\"jvm.thread.blocked.count\":{\"value\":0},\"jvm.thread.count\":{\"value\":82},\"jvm.thread.daemon.count\":{\"value\":11},\"jvm.thread.deadlock.count\":{\"value\":0},\"jvm.thread.deadlocks\":{\"value\":[]},\"jvm.thread.new.count\":{\"value\":0},\"jvm.thread.runnable.count\":{\"value\":25},\"jvm.thread.terminated.count\":{\"value\":0},\"jvm.thread.timed_waiting.count\":{\"value\":10},\"jvm.thread.waiting.count\":{\"value\":47}},\"counters\":{},\"histograms\":{},\"meters\":{},\"timers\":{}}\n\n$ curl -H 'Accept: application/prometheus' http://otoroshi-api.oto.tools:8080/metrics\\?access_key\\=MILpkVv6f2kG9Xmnc4mFIYRU4rTxHVGkxvB0hkQLZwEaZgE2hgbOXiRsN1DBnbtY\n# TYPE attr_jvm_cpu_usage gauge\nattr_jvm_cpu_usage 83.0\n# TYPE attr_jvm_heap_size gauge\nattr_jvm_heap_size 1409.0\n# TYPE attr_jvm_heap_used gauge\nattr_jvm_heap_used 220.0\n# TYPE internals_0_concurrent_requests gauge\ninternals_0_concurrent_requests 1.0\n# TYPE internals_global_throttling_quotas gauge\ninternals_global_throttling_quotas 3.0\n# TYPE jvm_attr_uptime gauge\njvm_attr_uptime 2372614.0\n# TYPE jvm_gc_PS_MarkSweep_count gauge\njvm_gc_PS_MarkSweep_count 3.0\n# TYPE jvm_gc_PS_MarkSweep_time gauge\njvm_gc_PS_MarkSweep_time 261.0\n# TYPE jvm_gc_PS_Scavenge_count gauge\njvm_gc_PS_Scavenge_count 12.0\n# TYPE jvm_gc_PS_Scavenge_time gauge\njvm_gc_PS_Scavenge_time 161.0\n# TYPE jvm_memory_heap_committed gauge\njvm_memory_heap_committed 1.477967872E9\n# TYPE jvm_memory_heap_init gauge\njvm_memory_heap_init 1.690304512E9\n# TYPE jvm_memory_heap_max gauge\njvm_memory_heap_max 3.005218816E9\n# TYPE jvm_memory_heap_usage gauge\njvm_memory_heap_usage 0.07680553268571043\n# TYPE jvm_memory_heap_used gauge\njvm_memory_heap_used 2.30817432E8\n# TYPE jvm_memory_non_heap_committed gauge\njvm_memory_non_heap_committed 1.66510592E8\n# TYPE jvm_memory_non_heap_init gauge\njvm_memory_non_heap_init 7667712.0\n# TYPE jvm_memory_non_heap_max gauge\njvm_memory_non_heap_max 9.94050048E8\n# TYPE jvm_memory_non_heap_usage gauge\njvm_memory_non_heap_usage 0.15262878997416435\n# TYPE jvm_memory_non_heap_used gauge\njvm_memory_non_heap_used 1.51720656E8\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_nmethods__committed 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_nmethods__max 5832704.0\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_nmethods__usage 0.28408093398876405\n# TYPE jvm_memory_pools_CodeHeap__non_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_nmethods__used 1656960.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__committed 1.1862016E7\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__usage 0.09610562183417755\n# TYPE jvm_memory_pools_CodeHeap__non_profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__non_profiled_nmethods__used 1.1812608E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__committed gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__committed 3.735552E7\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__init gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__init 2555904.0\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__max gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__max 1.22912768E8\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__usage gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__usage 0.25493618368435084\n# TYPE jvm_memory_pools_CodeHeap__profiled_nmethods__used gauge\njvm_memory_pools_CodeHeap__profiled_nmethods__used 3.1334912E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_committed gauge\njvm_memory_pools_Compressed_Class_Space_committed 1.4942208E7\n# TYPE jvm_memory_pools_Compressed_Class_Space_init gauge\njvm_memory_pools_Compressed_Class_Space_init 0.0\n# TYPE jvm_memory_pools_Compressed_Class_Space_max gauge\njvm_memory_pools_Compressed_Class_Space_max 3.670016E8\n# TYPE jvm_memory_pools_Compressed_Class_Space_usage gauge\njvm_memory_pools_Compressed_Class_Space_usage 0.03386023385184152\n# TYPE jvm_memory_pools_Compressed_Class_Space_used gauge\njvm_memory_pools_Compressed_Class_Space_used 1.242676E7\n# TYPE jvm_memory_pools_Metaspace_committed gauge\njvm_memory_pools_Metaspace_committed 9.9794944E7\n# TYPE jvm_memory_pools_Metaspace_init gauge\njvm_memory_pools_Metaspace_init 0.0\n# TYPE jvm_memory_pools_Metaspace_max gauge\njvm_memory_pools_Metaspace_max 3.75390208E8\n# TYPE jvm_memory_pools_Metaspace_usage gauge\njvm_memory_pools_Metaspace_usage 0.25170985813247426\n# TYPE jvm_memory_pools_Metaspace_used gauge\njvm_memory_pools_Metaspace_used 9.4489416E7\n# TYPE jvm_memory_pools_PS_Eden_Space_committed gauge\njvm_memory_pools_PS_Eden_Space_committed 3.49700096E8\n# TYPE jvm_memory_pools_PS_Eden_Space_init gauge\njvm_memory_pools_PS_Eden_Space_init 4.22576128E8\n# TYPE jvm_memory_pools_PS_Eden_Space_max gauge\njvm_memory_pools_PS_Eden_Space_max 1.110966272E9\n# TYPE jvm_memory_pools_PS_Eden_Space_usage gauge\njvm_memory_pools_PS_Eden_Space_usage 0.17698545577448457\n# TYPE jvm_memory_pools_PS_Eden_Space_used gauge\njvm_memory_pools_PS_Eden_Space_used 1.96624872E8\n# TYPE jvm_memory_pools_PS_Eden_Space_used_after_gc gauge\njvm_memory_pools_PS_Eden_Space_used_after_gc 0.0\n# TYPE jvm_memory_pools_PS_Old_Gen_committed gauge\njvm_memory_pools_PS_Old_Gen_committed 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_init gauge\njvm_memory_pools_PS_Old_Gen_init 1.1272192E9\n# TYPE jvm_memory_pools_PS_Old_Gen_max gauge\njvm_memory_pools_PS_Old_Gen_max 2.253914112E9\n# TYPE jvm_memory_pools_PS_Old_Gen_usage gauge\njvm_memory_pools_PS_Old_Gen_usage 0.014950035505168354\n# TYPE jvm_memory_pools_PS_Old_Gen_used gauge\njvm_memory_pools_PS_Old_Gen_used 3.3696096E7\n# TYPE jvm_memory_pools_PS_Old_Gen_used_after_gc gauge\njvm_memory_pools_PS_Old_Gen_used_after_gc 2.3791152E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_committed gauge\njvm_memory_pools_PS_Survivor_Space_committed 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_init gauge\njvm_memory_pools_PS_Survivor_Space_init 7.0254592E7\n# TYPE jvm_memory_pools_PS_Survivor_Space_max gauge\njvm_memory_pools_PS_Survivor_Space_max 1048576.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_usage gauge\njvm_memory_pools_PS_Survivor_Space_usage 0.59375\n# TYPE jvm_memory_pools_PS_Survivor_Space_used gauge\njvm_memory_pools_PS_Survivor_Space_used 622592.0\n# TYPE jvm_memory_pools_PS_Survivor_Space_used_after_gc gauge\njvm_memory_pools_PS_Survivor_Space_used_after_gc 622592.0\n# TYPE jvm_memory_total_committed gauge\njvm_memory_total_committed 1.644478464E9\n# TYPE jvm_memory_total_init gauge\njvm_memory_total_init 1.697972224E9\n# TYPE jvm_memory_total_max gauge\njvm_memory_total_max 3.999268864E9\n# TYPE jvm_memory_total_used gauge\njvm_memory_total_used 3.82665128E8\n# TYPE jvm_thread_blocked_count gauge\njvm_thread_blocked_count 0.0\n# TYPE jvm_thread_count gauge\njvm_thread_count 82.0\n# TYPE jvm_thread_daemon_count gauge\njvm_thread_daemon_count 11.0\n# TYPE jvm_thread_deadlock_count gauge\njvm_thread_deadlock_count 0.0\n# TYPE jvm_thread_new_count gauge\njvm_thread_new_count 0.0\n# TYPE jvm_thread_runnable_count gauge\njvm_thread_runnable_count 25.0\n# TYPE jvm_thread_terminated_count gauge\njvm_thread_terminated_count 0.0\n# TYPE jvm_thread_timed_waiting_count gauge\njvm_thread_timed_waiting_count 10.0\n# TYPE jvm_thread_waiting_count gauge\njvm_thread_waiting_count 47.0\n```"},{"name":"netty-server.md","id":"/topics/netty-server.md","url":"/topics/netty-server.html","title":"Alternative HTTP server","content":"# Alternative HTTP server\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nwith the change of licence in Akka, we are experimenting around using Netty as http server for otoroshi (and getting rid of akka http)\n\nin `v1.5.14` we are introducing a new alternative http server base on [`reactor-netty`](https://projectreactor.io/docs/netty/release/reference/index.html). It also include a preview of an HTTP3 server using [netty-incubator-codec-quic](https://github.com/netty/netty-incubator-codec-quic) and [netty-incubator-codec-http3](https://github.com/netty/netty-incubator-codec-http3)\n\n## The specs\n\nthis new server can start during otoroshi boot sequence and accept HTTP/1.1 (with and without TLS), H2C and H2 (with and without TLS) connections and supporting both standard HTTP calls and websockets calls.\n\n## Enable the server\n\nto enable the server, just turn on the following flag\n\n```conf\notoroshi.next.experimental.netty-server.enabled = true\n```\n\nnow you should see something like the following in the logs\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Server options\n\nyou can also setup the host and ports of the server using\n\n```conf\notoroshi.next.experimental.netty-server.host = \"0.0.0.0\"\notoroshi.next.experimental.netty-server.http-port = 10049\notoroshi.next.experimental.netty-server.https-port = 10048\n```\n\nyou can also enable access logs using\n\n```conf\notoroshi.next.experimental.netty-server.accesslog = true\n```\n\nand enable wiretaping using \n\n```conf\notoroshi.next.experimental.netty-server.wiretap = true\n```\n\nyou can also custom number of worker thread using\n\n```conf\notoroshi.next.experimental.netty-server.thread = 0 # system automatically assign the right number of threads\n```\n\n## HTTP2\n\nyou can enable or disable HTTP2 with\n\n```conf\notoroshi.next.experimental.netty-server.http2.enabled = true\notoroshi.next.experimental.netty-server.http2.h2c = true\n```\n\n## HTTP3\n\nyou can enable or disable HTTP3 (preview ;) ) with\n\n```conf\notoroshi.next.experimental.netty-server.http3.enabled = true\notoroshi.next.experimental.netty-server.http3.port = 10048 # yep can the the same as https because its on the UDP stack\n```\n\nthe result will be something like\n\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Native transport\n\nIt is possible to enable native transport for the server\n\n```conf\notoroshi.next.experimental.netty-server.native.enabled = true\notoroshi.next.experimental.netty-server.native.driver = \"Auto\"\n```\n\npossible values for `otoroshi.next.experimental.netty-server.native.driver` are \n\n- `Auto`: the server try to find the best native option available\n- `Epoll`: the server uses Epoll native transport for Linux environments\n- `KQueue`: the server uses KQueue native transport for MacOS environments\n- `IOUring`: the server uses IOUring native transport for Linux environments that supports it (experimental, using [netty-incubator-transport-io_uring](https://github.com/netty/netty-incubator-transport-io_uring))\n\nthe result will be something like when starting on a Mac\n\n```log\n...\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - Starting the experimental Netty Server !!!\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - using KQueue native transport\nroot [info] otoroshi-experimental-netty-server -\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/3)\nroot [info] otoroshi-experimental-netty-server - https://0.0.0.0:10048 (HTTP/1.1, HTTP/2)\nroot [info] otoroshi-experimental-netty-server - http://0.0.0.0:10049 (HTTP/1.1, HTTP/2 H2C)\nroot [info] otoroshi-experimental-netty-server -\n...\n```\n\n## Env. variables\n\nyou can configure the server using the following env. variables\n\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED`\n* `OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER`\n\n"},{"name":"opentelemetry.md","id":"/topics/opentelemetry.md","url":"/topics/opentelemetry.html","title":"OpenTelemetry support","content":"# OpenTelemetry support\n\nOpenTelemetry is an open-source project focused on providing a set of APIs, libraries, agents, and instrumentation to \nenable observability in modern software applications. It helps developers and software teams collect, process, \nand export telemetry data, which includes metrics, traces, and logs, from their applications and infrastructure. \nThe project aims to provide a standardized approach to instrumenting applications for distributed tracing, metrics, and logging.\n\nHere's a breakdown of the key components of OpenTelemetry:\n\n- **Tracing**: Distributed tracing is a method used to monitor and understand the flow of requests across different services \nin a distributed system. OpenTelemetry allows developers to add instrumentation to their code to trace requests as they \nflow through various services, providing insights into performance bottlenecks and dependencies between components.\n- **Metrics**: Metrics are quantitative measurements that provide information about the behavior and performance of \nan application. OpenTelemetry enables developers to collect metrics from their applications, such as CPU usage, memory \nconsumption, and custom application-specific metrics, to gain visibility into the application's health and performance.\n- **Logging**: OpenTelemetry also supports capturing and exporting logs, which are textual records of events and messages \nthat occur during the execution of an application. Logs are essential for debugging and monitoring purposes, and \nOpenTelemetry allows developers to integrate logging with other telemetry data, making it easier to correlate events.\n\nOpenTelemetry is designed to be language-agnostic and vendor-agnostic, supporting multiple programming languages and \nvarious telemetry backends. This flexibility makes it easier for developers to adopt the OpenTelemetry standard \nregardless of their technology stack.\n\nThe goal of OpenTelemetry is to promote a consistent way of collecting telemetry data across different applications \nand environments, making it easier for developers to adopt observability best practices. By leveraging OpenTelemetry, \nsoftware teams can gain deeper insights into the behavior of their systems and improve performance, troubleshoot \nissues, and enhance the overall reliability of their applications.\n\nNow, OpenTelemetry is officialy supported in Otoroshi and can be used in different parts of your instance. You can use \nit to collect otoroshi server logs and otoroshi server metrics through config. file. Then you have access to 2 new data \nexporter that can export otoroshi events to OpenTelemetry log collector and send custom metrics to an OpenTelemetry metrics collector.\n\n## server logs\n\notoroshi server logs can be sent to an OpenTelemetry log collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-logs.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the logs push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-logs {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## server metrics\n\notoroshi server metrics can be sent to an OpenTelemetry metrics collector. Everything is configured throught the config. file\nand can be overloaded through env. variables, and `-D` jvm flags.\n\nfirst you need to set the `otoroshi.open-telemetry.server-metrics.enabled` flag to `true` and then configure the remote \nconnection through endpoint, timeout, gzip and grpc. You can also enabled mTLS through `client_cert` and `trusted_cert` \nthat are otoroshi certificates id references. Finally you can use `max_duration` to specify the metrics push interval.\n\n```config\notoroshi {\n ...\n open-telemetry {\n server-metrics {\n enabled = false\n enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED}\n gzip = false\n gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP}\n grpc = false\n grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC}\n endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT}\n timeout = 5000\n timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT}\n client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT}\n trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT}\n headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS}\n max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION}\n }\n ...\n }\n ...\n}\n```\n\n## logs data expoter\n\nA new kind of data exporter is now available to send otoroshi events serialized as text to an OpenTelemetry log collector. \nFirst create a new data exporter and select the type `otlp-logs`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\n@@@ div { .centered-img }\n\n@@@\n\n## metrics data exporter\n\nA new kind of data exporter is now available to send custom metrics derived from otoroshi events to an OpenTelemetry metrics collector. \nFirst create a new data exporter and select the type `otlp-metrics`. Then fill the filter and projection part as needed. In\nthe exporter config. section, fill the collectors endpoint, timeout, gzip and grpc flags, enable mTLS through \n`client_cert` and `trusted_cert`. \n\nThen you will be able to add new metrics on this data exporter with a name, the type of metric (counter, timer, histogram), the value and the kind of event it's based on.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n\n\n"},{"name":"otoroshi-protocol.md","id":"/topics/otoroshi-protocol.md","url":"/topics/otoroshi-protocol.html","title":"The Otoroshi communication protocol","content":"# The Otoroshi communication protocol\n\nThe exchange protocol secure the communication with an app. When it's enabled, Otoroshi will send for each request a value in pre-selected token header, and will check the same header in the return request. On routes, you will have to use the `Otoroshi challenge token` plugin to enable it.\n\n### V1 challenge\n\nIf you enable secure communication for a given service with `V1 - simple values exchange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header and return it in a header named `Otoroshi-State-Resp`. \n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V1 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### V2 challenge\n\nIf you enable secure communication for a given service with `V2 - signed JWT token exhange` activated, you will have to add a filter on the target application that will take the `Otoroshi-State` header value containing a JWT token, verify it's content signature then extract a claim named `state` and return a new JWT token in a header named `Otoroshi-State-Resp` with the `state` value in a claim named `state-resp`. By default, the signature algorithm is HMAC+SHA512 but can you can choose your own. The sent and returned JWT tokens have short TTL to avoid being replayed. You must be validate the tokens TTL. The audience of the response token must be `Otoroshi` and you have to specify `iat`, `nbf` and `exp`.\n\n@@@ div { .centered-img }\n\n@@@\n\nyou can find an example project that implements V2 challenge [here](https://github.com/MAIF/otoroshi/tree/master/demos/challenge)\n\n### Info. token\n\nOtoroshi is also sending a JWT token in a header named `Otoroshi-Claim` that the target app can validate too. On routes, you will have to use the `Otoroshi info. token` plugin to enable it.\n\nThe `Otoroshi-Claim` is a JWT token containing some informations about the service that is called and the client if available. You can choose between a legacy version of the token and a new one that is more clear and structured.\n\nBy default, the otoroshi jwt token is signed with the `otoroshi.claim.sharedKey` config property (or using the `$CLAIM_SHAREDKEY` env. variable) and uses the `HMAC512` signing algorythm. But it is possible to customize how the token is signed from the service descriptor page in the `Otoroshi exchange protocol` section. \n\n@@@ div { .centered-img }\n\n@@@\n\nusing another signing algo.\n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can choose the signing algorithm and the secret/keys used. You can use syntax like `${env.MY_ENV_VAR}` or `${config.my.config.path}` to provide secret/keys values. \n\nFor example, for a service named `my-service` with a signing key `secret` with `HMAC512` signing algorythm, the basic JWT token that will be sent should look like the following\n\n```\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiItLSIsImF1ZCI6Im15LXNlcnZpY2UiLCJpc3MiOiJPdG9yb3NoaSIsImV4cCI6MTUyMTQ0OTkwNiwiaWF0IjoxNTIxNDQ5ODc2LCJqdGkiOiI3MTAyNWNjMTktMmFjNy00Yjk3LTljYzctMWM0ODEzYmM1OTI0In0.mRcfuFVFPLUV1FWHyL6rLHIJIu0KEpBkKQCk5xh-_cBt9cb6uD6enynDU0H1X2VpW5-bFxWCy4U4V78CbAQv4g\n```\n\nif you decode it, the payload will look something like\n\n```json\n{\n \"sub\": \"apikey_client_id\",\n \"aud\": \"my-service\",\n \"iss\": \"Otoroshi\",\n \"exp\": 1521449906,\n \"iat\": 1521449876,\n \"jti\": \"71025cc19-2ac7-4b97-9cc7-1c4813bc5924\"\n}\n```\n\nIf you want to validate the `Otoroshi-Claim` on the target app side to ensure that the input requests only comes from `Otoroshi`, you will have to write an HTTP filter to do the job. For instance, if you want to write a filter to make sure that requests only comes from Otoroshi, you can write something like the following (using playframework 2.6).\n\nScala\n: @@snip [filter.scala](../snippets/filter.scala)\n\nJava\n: @@snip [filter.java](../snippets/filter.java)\n"},{"name":"pki.md","id":"/topics/pki.md","url":"/topics/pki.html","title":"Otoroshi's PKI","content":"# Otoroshi's PKI\n\nWith Otoroshi, you can add your own certificates, your own CA and even create self signed certificates or certificates from CAs. You can enable auto renewal of thoses self signed certificates or certificates generated. Certificates have to be created with the certificate chain and the private key in PEM format.\n\nAn Otoroshi instance always starts with 5 auto-generated certificates. \n\nThe highest certificate is the **Otoroshi Default Root CA Certificate**. This certificate is used by Otoroshi to sign the intermediate CA.\n\n**Otoroshi Default Intermediate CA Certificate**: first intermediate CA that must be used to issue new certificates in Otoroshi. Creating certificates directly from the CA root certificate increases the risk of root certificate compromise, and if the CA root certificate is compromised, the entire trust infrastructure built by the SSL provider will fail\n\nThis intermediate CA signed three certificates :\n\n* **Otoroshi Default Client certificate**: \n* **Otoroshi Default Jwt Signing Keypair**: default keypair (composed of a public and private key), exposed on `https://xxxxxx/.well-known/jwks.json`, that can be used to sign and verify JWT verifier\n* **Otoroshi Default Wildcard Certificate**: this certificate has `*.oto.tools` as common name. It can be very useful to the development phase\n\n## The PKI API\n\nThe Otoroshi's PKI can be managed using the admin api of otoroshi (by default admin api is exposed on https://otoroshi-api.xxxxx)\n\nLink to the complete swagger section about PKI : https://maif.github.io/otoroshi/swagger-ui/index.html#/pki\n\n* `POST` [/api/pki/certs/_letencrypt](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genLetsEncryptCert): generates a certificate using Let's Encrypt or any ACME compatible system\n* `POST` [/api/pki/certs/_p12](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.importCertFromP12): import a .p12 file as client certificates\n* `POST` [/api/pki/certs/_valid](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateIsValid): check if a certificate is valid (based on its own data)\n* `POST` [/api/pki/certs/_data](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.certificateData): extract data from a certificate\n* `POST` [/api/pki/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCert): generates a self signed certificates\n* `POST` [/api/pki/csrs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCsr) : generates a CSR\n* `POST` [/api/pki/keys](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genKeyPair) : generates a keypair\n* `POST` [/api/pki/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSelfSignedCA) : generates a self signed CA\n* `POST` [/api/pki/cas/:ca/certs/_sign](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.signCert): sign a certificate based on CSR\n* `POST` [/api/pki/cas/:ca/certs](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genCert): generates a certificate\n* `POST` [/api/pki/cas/:ca/cas](https://maif.github.io/otoroshi/swagger-ui/index.html#/pki/otoroshi.controllers.adminapi.PkiController.genSubCA) : generates a sub-CA\n\n## The PKI UI\n\nAll generated certificates are listed in the `https://xxxxxx/bo/dashboard/certificates` page. All those certificates can be used to serve traffic with TLS, perform mTLS calls, sign and verify JWT tokens.\n\nThe PKI UI are composed of these following actions:\n\n* **Add item**: redirects the user on the certificate creation page. It’s useful when you already had a certificate (like a pem file) and that you want to load it in Otoroshi.\n* **Let's Encrypt certificate**: asks a certificate matching a given host to Let’s encrypt\n* **Create certificate**: issues a certificate with an existing Otoroshi certificate as CA. You can create a client certificate, a server certificate or a keypair certiciate that will be used to verify and sign JWT tokens.\n* **Import .p12 file**: loads a p12 file as certificate\n\nUnder these buttons, you have the list of current certificates, imported or generated, revoked or not. For each certificate, you will find: \n\n* a **name** \n* a **description** \n* the **subject** \n* the **type** of certificate (CA / client / keypair / certificate)\n* the **revoked reason** (empty if not) \n* the **creation date** following by its **expiration date**.\n\n## Exposed public keys\n\nThe Otoroshi certificate can be turned and used as keypair (simple action that can be executed by editing a certificate or during its creation, or using the admin api). A Otoroski keypair can be used to sign and verify JWT tokens with asymetric signature. Once a jwt token is signed with a keypair, it can be necessary to provide a way to the services to verify the tokens received by Otoroshi. This usage is cover by Otoroshi by the flag `Public key exposed`, available on each certificate.\n\nOtoroshi exposes each keypair with the flag enabled, on the following routes:\n\n* `https://xxxxxxxxx.xxxxxxx.xx/.well-known/otoroshi/security/jwks.json`\n* `https://otoroshi-api.xxxxxxx.xx/.well-known/jwks.json`\n\nOn these routes, you will find the list of public keys exposed using [the JWK standard](https://datatracker.ietf.org/doc/html/rfc7517)\n\n\n## OCSP Responder\n\nOtoroshi is able to revocate a certificate, directly from the UI, and to add a revocation status to specifiy the reason. The revocation reason can be :\n\n* `VALID`: The certificate is not revoked\n* `UNSPECIFIED`: Can be used to revoke certificates for reasons other than the specific codes.\n* `KEY_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `CA_COMPROMISE`: It is known or suspected that the subject's private key or other aspects have been compromised.\n* `AFFILIATION_CHANGED`: The subject's name or other information in the certificate has been modified but there is no cause to suspect that the private key has been compromised.\n* `SUPERSEDED`: The certificate has been superseded but there is no cause to suspect that the private key has been compromised\n* `CESSATION_OF_OPERATION`: The certificate is no longer needed for the purpose for which it was issued but there is no cause to suspect that the private key has been compromised\n* `CERTIFICATE_HOLD`: The certificate is temporarily revoked but there is no cause to suspect that the private kye has been compromised\n* `REMOVE_FROM_CRL`: The certificate has been unrevoked\n* `PRIVILEGE_WITH_DRAWN`: The certificate was revoked because a privilege contained within that certificate has been withdrawn\n* `AA_COMPROMISE`: It is known or suspected that aspects of the AA validated in the attribute certificate, have been compromised\n\nOtoroshi supports the Online Certificate Status Protocol for obtaining the revocation status of its certificates. The OCSP endpoint is also add to any generated certificate. This endpoint is available at `https://otoroshi-api.xxxxxx/.well-known/otoroshi/security/ocsp`\n\n## A.I.A : Authority Information Access\n\nOtoroshi provides a way to add the A.I.A in the certificate. This certificate extension contains :\n\n* Information about how to get the issuer of this certificate (CA issuer access method)\n* Address of the OCSP responder from where revocation of this certificate can be checked (OCSP access method)\n\n`https://xxxxxxxxxx/.well-known/otoroshi/security/certificates/:cert-id`"},{"name":"relay-routing.md","id":"/topics/relay-routing.md","url":"/topics/relay-routing.html","title":"Relay Routing","content":"# Relay Routing\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nRelay routing is the capability to forward traffic between otoroshi leader nodes based on network location of the target. Let say we have an otoroshi cluster split accross 3 network zones. Each zone has \n\n- one or more datastore instances\n- one or more otoroshi leader instances\n- one or more otoroshi worker instances\n\nthe datastores are replicated accross network zones in an active-active fashion. Each network zone also have applications, apis, etc deployed. Sometimes the same application is deployed in multiple zones, sometimes not. \n\nit can quickly become a nightmare when you want to access an application deployed in one network zone from another network zone. You'll have to publicly expose this application to be able to access it from the other zone. This pattern is fine, but sometimes it's not enough. With `relay routing`, you will be able to flag your routes as being deployed in one zone or another, and let otoroshi handle all the heavy lifting to route the traffic to the right network zone for you.\n\n@@@ div { .centered-img }\n\n@@@\n\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through relay nodes\n@@@\n\n## Otoroshi instance setup\n\nfirst of all, for every otoroshi instance deployed, you have to flag where the instance is deployed and, for leaders, how this instance can be contacted from other zones (this is a **MAJOR** requirement, without that, you won't be able to make relay routing work). Also, you'll have to enable the @ref:[new proxy engine](./engine.md).\n\nIn the otoroshi configuration file, for each instance, enable relay routing and configure where the instance is located and how the leader can be contacted\n\n```conf\notoroshi {\n ...\n cluster {\n mode = \"leader\" # or \"worker\" dependending on the instance kind\n ...\n relay {\n enabled = true # enable relay routing\n leaderOnly = true # use leaders as the only kind of relay node\n location { # you can use all those parameters at the same time. There is no actual network concepts bound here, just some kind of tagging system, so you can use it as you wish\n provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER}\n zone = \"zone-1\"\n region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION}\n datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER}\n rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK}\n }\n exposition {\n urls = [\"https://otoroshi-api-zone-1.my.domain:443\"]\n hostname = \"otoroshi-api-zone-1.my.domain\"\n clientId = \"apkid_relay-routing-apikey\"\n }\n }\n }\n}\n```\n\nalso, to make your leaders exposed by zone, do not hesitate to add domain names to the `otoroshi-admin-api` service and setup your DNS to bind those domains to the right place\n\n@@@ div { .centered-img }\n\n@@@\n\n## Route setup for an application deployed in only one zone\n\nNow, for any route/service deployed in only one zone, you will be able to flag it using its metadata as being deployed in one zone or another. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1`\n\n## Route setup for an application deployed in multiple zones at the same time\n\nNow, for any route/service deployed in multiple zones zones at the same time, you will be able to flag it using its metadata as being deployed in some zones. The possible metadata keys are the following\n\n- `otoroshi-deployment-providers`\n- `otoroshi-deployment-regions`\n- `otoroshi-deployment-zones`\n- `otoroshi-deployment-dcs`\n- `otoroshi-deployment-racks`\n\nlet say we set `otoroshi-deployment-zones=zone-1, zone-2` on a route, if we call this route from an otoroshi instance where `otoroshi.cluster.relay.location.zone` is not `zone-1` or `zone-2`, otoroshi will automatically forward the requests to an otoroshi leader node where `otoroshi.cluster.relay.location.zone` is `zone-1` or `zone-2` and load balance between them.\n\nalso, you will have to setup your targets to avoid trying to contact targets that are not actually in the current zone. To do that, you'll have to set the target predicate to `NetworkLocationMatch` and fill the possible locations according to the actual location of your target\n\n@@@ div { .centered-img }\n\n@@@\n\n## Demo\n\nyou can find a demo of this setup [here](https://github.com/MAIF/otoroshi/tree/master/demos/relay). This is a `docker-compose` setup with multiple network to simulate network zones. You also have an otoroshi export to understand how to setup your routes/services\n"},{"name":"secrets.md","id":"/topics/secrets.md","url":"/topics/secrets.html","title":"Secrets management","content":"# Secrets management\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSecrets are generally confidential values that should not appear in plain text in the application. There are several products that help you store, retrieve, and rotate these secrets securely. Otoroshi offers a mechanism to set up references to these secrets in its entities to benefits from the perks of your existing secrets management infrastructure. This feature only work with the @ref:[new proxy engine](./engine.md).\n\nA secret can be anything you want like an apikey secret, a certificate private key or password, a jwt verifier signing key, a password to a proxy, a value for a header, etc.\n\n## Enable secrets management in otoroshi\n\nBy default secrets management is disbaled. You can enable it by setting `otoroshi.vaults.enabled` or `${OTOROSHI_VAULTS_ENABLED}` to `true`.\n\n## Global configuration\n\nSecrets management can be only configured using otoroshi static configuration file (also using jvm args mechanism). \nThe configuration is located at `otoroshi.vaults` where you can find the global configuration of the secrets management system and the configurations for each enabled secrets management backends. Basically it looks like\n\n```conf\nvaults {\n enabled = false\n enabled = ${?OTOROSHI_VAULTS_ENABLED}\n secrets-ttl = 300000 # 5 minutes\n secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL}\n cached-secrets = 10000\n cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS}\n read-timeout = 10000 # 10 seconds\n read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT}\n # if enabled, only leader nodes fetches the secrets.\n # entities with secret values filled are then sent to workers when they poll the cluster state.\n # only works if `otoroshi.cluster.autoUpdateState=true`\n leader-fetch-only = false\n leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY}\n env {\n type = \"env\"\n prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX}\n }\n}\n```\n\nyou can see here the global configuration and a default backend configured that can retrieve secrets from environment variables. \n\nThe configuration keys can be used for \n\n- `secrets-ttl`: the amount of milliseconds before the secret value is read again from backend\n- `cached-secrets`: the number of secrets that will be cached on an otoroshi instance\n- `read-timeout`: the timeout (in milliseconds) to read a secret from a backend\n\n## Entities with secrets management\n\nthe entities that support secrets management are the following \n\n- `routes`\n- `services`\n- `service_descriptors`\n- `apikeys`\n- `certificates`\n- `jwt_verifiers`\n- `authentication_modules`\n- `targets`\n- `backends`\n- `tcp_services`\n- `data_exporters`\n\n## Define a reference to a secret\n\nin the previously listed entities, you can define, almost everywhere, references to a secret using the following syntax:\n\n`${vault://name_of_the_vault/secret/of/the/path}`\n\nlet say I define a new apikey with the following value as secret `${vault://my_env/apikey_secret}` with the following secrets management configuration\n\n```conf\nvaults {\n enabled = true\n secrets-ttl = 300000\n cached-secrets = 10000\n read-ttl = 10000\n my_env {\n type = \"env\"\n }\n}\n```\n\nif the machine running otoroshi has an environment variable named `APIKEY_SECRET` with the value `verysecret`, then you will be able to can an api with the defined apikey `client_id` and a `client_secret` value of `verysecret`\n\n```sh\ncurl 'http://my-awesome-api.oto.tools:8080/api/stuff' -u awesome_apikey:verysecret\n```\n\n## Possible backends\n\nOtoroshi comes with the support of several secrets management backends.\n\n### Environment variables\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"env\"\n prefix = \"the_prefix_added_to_the_name_of_the_env_variable\"\n }\n}\n```\n\n### Local\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"local\"\n root = \"the_root_path/in_otoroshi/environment\"\n }\n}\n```\n\nvalue of this vault can be configured in the danger zone > Global metadata > Otoroshi environment.\n\n### Infisical\n\na backend for the awesome open source project [Infisical](https://infisical.com/). It support both E2EE and non E2EE secrets.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"infisical\"\n baseUrl = \"https://app.infisical.com\" # optional, the base url of your infisical server, fallbacks to https://app.infisical.com\n serviceToken = \"st.xxxx.yyyy.zzzz\" # the service token for your projet\n e2ee = true # are you secrets end to end encrypted\n defaultSecretType = \"shared\" # optional, fallbacks to shared\n defaultWorkspaceId = \"xxxxxx\" # optional, value can be passed in the secret address\n defaultEnvironment = \"dev\" # optional, value can be passed in the secret address\n }\n}\n```\n\nyou should define your references like `${vault://infisical_vault/my_secret_path?workspaceId=xxx&environment=dev&type=shared}`. `workspaceId`, `environment` and `type` are optional if filled in global config. \n\nYou can also pass a `json_pointer=/foo/bar` to handle the value like a json document a select a value inside it.\n\n### Hashicorp Vault\n\na backend for [Hashicorp Vault](https://www.vaultproject.io/). Right now we only support KV engines.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"hashicorp-vault\"\n url = \"http://127.0.0.1:8200\"\n mount = \"kv\" # the name of the secret store in vault\n kv = \"v2\" # the version of the kv store (v1 or v2)\n token = \"root\" # the token that can access to your secrets\n }\n}\n```\n\nyou should define your references like `${vault://hashicorp_vault/secret/path/key_name}`.\n\n\n### Azure Key Vault\n\na backend for [Azure Key Vault](https://azure.microsoft.com/en-en/services/key-vault/). Right now we only support secrets and not keys and certificates.\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"azure\"\n url = \"https://keyvaultname.vault.azure.net\"\n api-version = \"7.2\" # the api version of the vault\n tenant = \"xxxx-xxx-xxx\" # your azure tenant id, optional\n client_id = \"xxxxx\" # your azure client_id\n client_secret = \"xxxxx\" # your azure client_secret\n # token = \"xxx\" possible if you have a long lived existing token. will take over tenant / client_id / client_secret\n }\n}\n```\n\nyou should define your references like `${vault://azure_vault/secret_name/secret_version}`. `secret_version` is mandatory\n\nIf you want to use certificates and keys objects from the azure key vault, you will have to specify an option in the reference named `azure_secret_kind` with possible value `certificate`, `privkey`, `pubkey` like the following :\n\n```\n${vault://azure_vault/myprivatekey/secret_version?azure_secret_kind=privkey}\n```\n\n### AWS Secrets Manager\n\na backend for [AWS Secrets Manager](https://aws.amazon.com/en/secrets-manager/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"aws\"\n access-key = \"key\"\n access-key-secret = \"secret\"\n region = \"eu-west-3\" # the aws region of your secrets management\n }\n}\n```\n\nyou should define your references like `${vault://aws_vault/secret_name/secret_version}`. `secret_version` is optional\n\n### Google Cloud Secrets Manager\n\na backend for [Google Cloud Secrets Manager](https://cloud.google.com/secret-manager)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"gcloud\"\n url = \"https://secretmanager.googleapis.com\"\n apikey = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://gcloud_vault/projects/foo/secrets/bar/versions/the_version}`. `the_version` can be `latest`\n\n### AlibabaCloud Cloud Secrets Manager\n\na backend for [AlibabaCloud Secrets Manager](https://www.alibabacloud.com/help/en/doc-detail/152001.html)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"alibaba-cloud\"\n url = \"https://kms.eu-central-1.aliyuncs.com\"\n access-key-id = \"access-key\"\n access-key-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://alibaba_vault/secret_name}`\n\n\n### Kubernetes Secrets\n\na backend for [Kubernetes secrets](https://kubernetes.io/en/docs/concepts/configuration/secret/)\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"kubernetes\"\n # see the configuration of the kubernetes plugin, \n # by default if the pod if well configured, \n # you don't have to setup anything\n }\n}\n```\n\nyou should define your references like `${vault://k8s_vault/namespace/secret_name/key_name}`. `key_name` is optional. if present, otoroshi will try to lookup `key_name` in the secrets `stringData`, if not defined the secrets `data` will be base64 decoded and used.\n\n\n### Izanami config.\n\na backend for [Izanami config.](https://maif.github.io/izanami/manual/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"izanami\"\n url = \"http://127.0.0.1:8200\"\n client-id = \"client\"\n client-secret = \"secret\"\n }\n}\n```\n\nyou should define your references like `${vault://izanami_vault/the:secret:id/key_name}`. `key_name` is optional if the secret value is not a json object\n\n### Spring Cloud Config\n\na backend for [Spring Cloud Config.](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)\n\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"spring-cloud\"\n url = \"http://127.0.0.1:8000\"\n root = \"myapp/prod\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://spring_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n\n### Http backend\n\na backend for that uses the result of an http endpoint\n\nthe configuration of this backend should be like\n\n```conf\nvaults {\n ...\n name_of_the_vault {\n type = \"http\"\n url = \"http://127.0.0.1:8000/endpoint/for/config\"\n headers {\n authorization = \"Basic xxxx\"\n }\n }\n}\n```\n\nyou should define your references like `${vault://http_vault/the/path/of/the/value}` where `/the/path/of/the/value` is the path of the value.\n"},{"name":"sessions-mgmt.md","id":"/topics/sessions-mgmt.md","url":"/topics/sessions-mgmt.html","title":"Sessions management","content":"# Sessions management\n\n## Admins\n\nAll logged users to an Otoroshi instance are administrators. An user session is created for each sucessfull connection to the UI. \n\nThese sessions are listed in the `Admin users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/admin`).\n\nAn admin user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Rights`: list of rules to authorize the connected user on each tenant and teams.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n\nIn the `Admin users sessions` page, you have two more actions:\n\n* `Discard all sessions`: kills all current sessions (including the session of the owner of this action)\n* `Discard old sessions`: kill all outdated sessions\n\n## Private apps\n\nAll logged users to a protected application has an private user session.\n\nThese sessions are listed in the `Private apps users sessions` (available in the cog icon menu or at this location of your instance `/bo/dashboard/sessions/private`).\n\nAn private user session is composed of: \n\n* `name`: the name of the connected user\n* `email`: the unique email\n* `Created at`: the creation date of the user session\n* `Expires at`: date until the user session is drop\n* `Profile`: user profile, at JSON format, containing name, email and others linked metadatas\n* `Meta.`: list of metadatas added by the authentication module.\n* `Tokens`: list of tokens received from the identity provider used. In the case of a memory authentication, this part will keep empty.\n* `Discard session`: action to kill a session. On click, a modal will appear with the session ID\n"},{"name":"tls.md","id":"/topics/tls.md","url":"/topics/tls.html","title":"TLS","content":"# TLS\n\nas you might have understand, otoroshi can store TLS certificates and use them dynamically. It means that once a certificate is imported or created in otoroshi, you can immediately use it to serve http request over TLS, to call https backends that requires mTLS or that do not have certicates signed by a globally knowned authority.\n\n## TLS termination\n\nany certficate added to otoroshi with a valid `CN` and `SANs` can be used in the following seconds to serve https requests. If you do not provide a private key with a certificate chain, the certificate will only be trusted like a CA. If you want to perform mTLS calls on you otoroshi instance, do not forget to enabled it (it is disabled by default for performance reasons as the TLS handshake is bigger with mTLS enabled)\n\n```sh\notoroshi.ssl.fromOutside.clientAuth=None|Want|Need\n```\n\nor using env. variables\n\n```sh\nSSL_OUTSIDE_CLIENT_AUTH=None|Want|Need\n```\n\n### TLS termination configuration\n\nYou can configure TLS termination statically using config. file or env. variables. Everything is available at `otoroshi.tls`\n\n```conf\notoroshi {\n tls {\n # the cipher suites used by otoroshi TLS termination\n cipherSuitesJDK11 = [\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuitesJDK8 = [\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_RSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_RSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_RSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA\", \"TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA\", \"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA\", \"TLS_EMPTY_RENEGOTIATION_INFO_SCSV\"]\n cipherSuites = []\n # the protocols used by otoroshi TLS termination\n protocolsJDK11 = [\"TLSv1.3\", \"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"]\n protocolsJDK8 = [\"SSLv2Hello\", \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n protocols = []\n # the JDK cacert access\n cacert {\n path = \"$JAVA_HOME/lib/security/cacerts\"\n password = \"changeit\"\n }\n # the mtls mode\n fromOutside {\n clientAuth = \"None\"\n clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH}\n }\n # the default trust mode\n trust {\n all = false\n all = ${?OTOROSHI_SSL_TRUST_ALL}\n }\n # some initial cacert access, useful to include non standard CA when starting (file paths)\n initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT}\n initialCacert = ${?INITIAL_CACERT}\n initialCert = ${?CLUSTER_WORKER_INITIAL_CERT}\n initialCert = ${?INITIAL_CERT}\n initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY}\n initialCertKey = ${?INITIAL_CERT_KEY}\n # initialCerts = [] \n }\n}\n```\n\n\n### TLS termination settings\n\nIt is possible to adjust the behavior of the TLS termination from the `danger zone` at the `Tls Settings` section. Here you can either define that a non-matching SNI call will use a random TLS certtificate to reply or will use a default domain (the TLS certificate associated to this domain) to reply. Here you can also choose if you want to trust all the CAs trusted by your JDK when performing TLS calls `Trust JDK CAs (client)` or when receiving mTLS calls `Trust JDK CAs (server)`. If you disable the later, it is possible to select the list of CAs presented to the client during mTLS handshake.\n\n### Certificates auto generation\n\nit is also possible to generate non-existing certificate on the fly without losing the request. If you are interested by this feature, you can enable it in the `danger zone` at the `Auto Generate Certificates` section. Here you'll have to enable it and select the CA that will generate the certificate. Of course, the client will have to trust the selected CA. You can also add filters to choose which domain are allowed to generate certificates or not. The `Reply Nicely` flag is used to reply a nice error message (ie. human readable) telling that it's not possible to have an auto certficate for the current domain. \n\n## Backends TLS and mTLS calls\n\nFor any call to a backend, it is possible to customize the TLS behavior \n\n@@@ div { .centered-img }\n\n@@@\n\nhere you can define your level of trust (trust all, loose verification) or even select on or more CAs you will trust for the following backend calls. You can also select the client certificate that will be used for the following backend calls\n\n## Keypair for signing and verification\n\nIt is also possible to use the keypair contained in a certificate to sign and verificate JWT token signature. You can mark an existing certificate in otoroshi as a keypair using the `keypair` on the certificate page.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"tunnels.md","id":"/topics/tunnels.md","url":"/topics/tunnels.html","title":"Otoroshi tunnels","content":"# Otoroshi tunnels\n\n@@include[experimental.md](../includes/experimental.md) { .experimental-feature }\n\nSometimes, exposing apis that lives in our private network can be a nightmare, especially from a networking point of view. \nWith otoroshi tunnels, this is now trivial, as long as your internal otoroshi (that lives inside your private network) is able to contact an external otoroshi (exposed on the internet).\n\n@@@ warning { .margin-top-20 }\nYou have to enable cluster mode (Leader or Worker) to make this feature work. As this feature is experimental, we only support simple http request right now. Server Sent Event and Websocket request are not supported at the moment.\n@@@\n\n## How Otoroshi tunnels works\n\nthe main idea behind otoroshi tunnels is that the connection between your private network et the public network is initiated by the private network side. You don't have to expose a part of your private network, create a DMZ or whatever, you just have to authorize your private network otoroshi instance to contact your public network otoroshi instance.\n\n@@@ div { .centered-img }\n\n@@@\n\nonce the persistent tunnel has been created, you can create routes on the public otoroshi instance that uses the otoroshi `Remote tunnel calls` to target your remote routes through the designated tunnel instance \n\n\n@@@ div { .centered-img }\n\n@@@\n\n@@@ warning { .margin-top-20 }\nthis feature may introduce additional latency as the call passes through otoroshi tunnels\n@@@\n\n## Otoroshi tunnel example\n\nfirst you have to enable the tunnels feature in your otoroshi configuration (on both public and private instances)\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n enabled = ${?OTOROSHI_TUNNELS_ENABLED}\n ...\n }\n}\n```\n\nthen you can setup a tunnel instance on your private instance to contact your public instance\n\n```conf\notoroshi {\n ...\n tunnels {\n enabled = true\n ...\n public-apis {\n id = \"public-apis\"\n name = \"public apis tunnel\"\n url = \"https://otoroshi-api.company.com:443\"\n host = \"otoroshi-api.company.com\"\n clientId = \"xxx\"\n clientSecret = \"xxxxxx\"\n # ipAddress = \"127.0.0.1\" # optional: ip address of the public instance admin api\n # tls { # optional: TLS settings to access the public instance admin api\n # ... \n # }\n # export-routes = true # optional: send routes information to remote otoroshi instance to facilitate remote route exposition\n # export-routes-tag = \"tunnel-exposed\" # optional: only send routes information if the route has this tag\n }\n }\n}\n```\n\nNow when your private otoroshi instance will boot, a persistent tunnel will be made between private and public instance. \nNow let say you have a private api exposed on `api-a.company.local` on your private otoroshi instance and you want to expose it on your public otoroshi instance. \n\nFirst create a new route exposed on `api-a.company.com` that targets `https://api-a.company.local:443`\n\n@@@ div { .centered-img }\n\n@@@\n\nthen add the `Remote tunnel calls` plugin to your route and set the tunnel id to `public-apis` to match the id you set in the otoroshi config file\n\n@@@ div { .centered-img }\n\n@@@\n\nadd all the plugin you need to secure this brand new public api and call it\n\n```sh\ncurl \"https://api-a.company.com/users\" | jq\n```\n\n## Easily expose your remote services\n\nyou can see all the connected tunnel instances on an otoroshi instance on the `Connected tunnels` (`Cog icon` / `Connected tunnels`). For each tunnel instance you will be able to check the tunnel health and also to easily expose all the routes available on the other end of the tunnel. Just clic on the `expose` button of the route you want to expose, and a new route will be created with the `Remote tunnel calls` plugin already setup.\n\n@@@ div { .centered-img }\n\n@@@\n"},{"name":"user-rights.md","id":"/topics/user-rights.md","url":"/topics/user-rights.html","title":"Otoroshi user rights","content":"# Otoroshi user rights\n\nIn Otoroshi, all users are considered **Administrators**. This choice is reinforced by the fact that Otoroshi is designed to be an administrator user interface and not an interface for users who simply want to view information. For this type of use, we encourage to use the admin API rather than giving access to the user interface.\n\nThe Otoroshi rights are split by a list of authorizations on **organizations** and **teams**. \n\nLet's taking an example where we want to authorize an administrator user on all organizations and teams.\n\nThe list of rights will be :\n\n```json\n[\n {\n \"tenant\": \"*:rw\", # (1)\n \"teams\": [\"*:rw\"] # (2)\n }\n]\n```\n\n* (1): this field, separated by a colon, indicates the name of the tenant and the associated rights. In our case, we set `*` to apply the rights to all tenants, and the `rw` to get the read and write access on them.\n* (2): the `teams` array field, represents the list of rights, applied by team. The behaviour is the same as the tenant field, we define the team or the wildcard, followed by the rights\n\nif you want to have an user that is administrator only for one organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that is administrator only for two organization, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\"*:rw\"]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\"*:rw\"]\n }\n]\n```\n\nif you want to have an user that can only see 3 teams of one organization and one team in the other, the rights will be :\n\n```json\n[\n {\n \"tenant\": \"orga-1:rw\",\n \"teams\": [\n \"team-1:rw\",\n \"team-2:rw\",\n \"team-3:rw\",\n ]\n },\n {\n \"tenant\": \"orga-2:rw\",\n \"teams\": [\n \"team-4:rw\"\n ]\n }\n]\n```\n\nThe list of possible rights for an organization or a team is:\n\n* **r**: read access\n* **w**: write access\n* **not**: none access to the resource\n\nThe list of possible tenant and teams are your created tenants and teams, and the wildcard to define rights to all resources once.\n\nThe user rights is defined by the @ref:[authentication modules](../entities/auth-modules.md).\n"},{"name":"wasm-usage.md","id":"/topics/wasm-usage.md","url":"/topics/wasm-usage.html","title":"Otoroshi and WASM","content":"# Otoroshi and WASM\n\nWebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. These plugins are:\n\n- `WasmRouteMatcher`: useful to define if a route can handle a request\n- `WasmPreRoute`: useful to check request and extract useful stuff for the other plugins\n- `WasmAccessValidator`: useful to control access to a route (jump to the next section to learn more about it)\n- `WasmRequestTransformer`: transform the content of an incoming request (body, headers, etc ...)\n- `WasmBackend`: execute a WASM file as Otoroshi target. Useful to implement user defined logic and function at the edge\n- `WasmResponseTransformer`: transform the content of the response produced by the target\n- `WasmSink`: create a sink plugin to handle unmatched requests\n- `WasmRequestHandler`: create a plugin that can handle the whole request lifecycle\n- `WasmJob`: create a job backed by a wasm function\n\nTo simplify the process of WASM creation and usage, Otoroshi provides:\n\n- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route\n- otoroshi `wasmo`: a code editor in the browser that let you write your plugin in `Rust`, `TinyGo`, `Javascript` or `Assembly Script` without having to think about compiling it to WASM (you can find a complete tutorial about it @ref:[here](../how-to-s/wasmo-installation.md))\n\n@@@ div { .centered-img }\n\n@@@\n\n## Available tutorials\n\nhere is the list of available tutorials about wasm in Otoroshi\n\n1. @ref:[Install a Wasmo](../how-to-s/wasmo-installation.md)\n2. @ref:[Use a WASM plugin](../how-to-s/wasm-usage.md)\n\n## Wasm plugins entities\n\nOtoroshi provides a dedicated entity for wasm plugins. Those entities makes it easy to declare a wasm plugin with specific configuration only once and use it in multiple places. \n\nYou can find wasm plugin entities at `/bo/dashboard/wasm-plugins`\n\nIn a wasm plugin entity, you can define the source of your wasm plugin. You can choose between\n\n- `base64`: a base64 encoded wasm script\n- `file`: the path to a wasm script file\n- `http`: the url to a wasm script file\n- `wasmo`: the name of a wasm script compiled by a Wasmo instance\n\nthen you can define the number of memory pages available for each plugin instanciation, the name of the function you want to invoke, the config. map of the VM and if you want to keep a wasm vm alive during the request lifecycle to be able to reuse it in different plugin steps\n\n@@@ div { .centered-img }\n\n@@@\n\n## Otoroshi plugins api\n\nthe following parts illustrates the apis for the different plugins. Otoroshi uses [Extism](https://extism.org/) to handle content sharing between the JVM and the wasm VM. All structures are sent to/from the plugins as json strings. \n\nfor instance, if we want to write a `WasmBackendCall` plugin using javascript, we could write something like\n\n```js\nfunction backend_call() {\n const input_str = Host.inputString(); // here we get the context passed by otoroshi as json string\n const backend_call_context = JSON.parse(input_str); // and parse it\n if (backend_call_context.path === '/hello') {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n message: `Hello ${ctx.request.query.name[0]}!` \n },\n status: 200,\n }));\n } else {\n Host.outputString(JSON.stringify({ // now we return a json string to otoroshi with the \"backend\" call result\n headers: { \n 'content-type': 'application/json' \n },\n body_json: { \n error: \"not found\"\n },\n status: 404,\n }));\n }\n return 0; // we return 0 to tell otoroshi that everything went fine\n}\n```\n\nthe following examples are written in rust. the rust macros provided by extism makes the usage of `Host.inputString` and `Host.outputString` useless. Remember that it's still used under the hood and that the structures are passed as json strings.\n\ndo not forget to add the extism pdk library to your project to make it compile\n\nCargo.toml\n: @@snip [Cargo.toml](../snippets/wasmo/Cargo.toml) \n\ngo.mod\n: @@snip [go.mod](../snippets/wasmo/go.mod) \n\npackage.json\n: @@snip [package.json](../snippets/wasmo/package.json) \n\n### WasmRouteMatcher\n\nA route matcher is a plugin that can help the otoroshi router to select a route instance based on your own custom predicate. Basically it's a function that returns a boolean answer.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn matches_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmMatchRouteResponse {\n pub result: bool,\n}\n```\n\n### WasmPreRoute\n\nA pre-route plugin can be used to short-circuit a request or enrich it (maybe extracting your own kind of auth. token, etc) a the very beginning of the request handling process, just after the routing part, when a route has bee chosen by the otoroshi router.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn pre_route(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteContext {\n pub snowflake: Option,\n pub route: Route,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmPreRouteResponse {\n pub error: bool,\n pub attrs: Option>,\n pub status: Option,\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmAccessValidator\n\nAn access validator plugin is typically used to verify if the request can continue or must be cancelled. For instance, the otoroshi apikey plugin is an access validator that check if the current apikey provided by the client is legit and authorized on the current route.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_access(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorContext {\n pub snowflake: Option,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorError {\n pub message: String,\n pub status: u32,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmAccessValidatorResponse {\n pub result: bool,\n pub error: Option,\n}\n```\n\n### WasmRequestTransformer\n\nA request transformer plugin can be used to compose or transform the request that will be sent to the backend\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmRequestTransformerContext {\n pub snowflake: Option,\n pub raw_request: OtoroshiRequest,\n pub otoroshi_request: OtoroshiRequest,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n}\n```\n\n### WasmBackendCall\n\nA backend call plugin can be used to simulate a backend behavior in otoroshi. For instance the static backend of otoroshi return the content of a file\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn call_backend(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmBackendContext {\n pub snowflake: Option,\n pub backend: Backend,\n pub apikey: Option,\n pub user: Option,\n pub raw_request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub request_body_bytes: Option>,\n pub request: OtoroshiRequest,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmBackendResponse {\n pub headers: Option>,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n pub status: u32,\n}\n```\n\n### WasmResponseTransformer\n\nA response transformer plugin can be used to compose or transform the response that will be sent back to the client\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn transform_response(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmResponseTransformerContext {\n pub snowflake: Option,\n pub raw_response: OtoroshiResponse,\n pub otoroshi_response: OtoroshiResponse,\n pub apikey: Option,\n pub user: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub route: Route,\n pub response_body_bytes: Option>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmTransformerResponse {\n pub headers: HashMap,\n pub cookies: Value,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmSink\n\nA sink is a kind of plugin that can be used to respond to any unmatched request before otoroshi sends back a 404 response\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn sink_matches(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn sink_handle(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkContext {\n pub snowflake: Option,\n pub request: RawRequest,\n pub config: Value,\n pub global_config: Value,\n pub attrs: Value,\n pub origin: String,\n pub status: u32,\n pub message: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkMatchesResponse {\n pub result: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WasmSinkHandleResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmRequestHandler\n\nA request handler is a very special kind of plugin that can bypass the otoroshi proxy engine on specific domains and completely handles the request/response lifecycle on it's own.\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn can_handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[plugin_fn]\npub fn handle_request(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerContext {\n pub request: RawRequest\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmRequestHandlerResponse {\n pub status: u32,\n pub headers: HashMap,\n pub body_bytes: Option>,\n pub body_base64: Option,\n pub body_json: Option,\n pub body_str: Option,\n}\n```\n\n### WasmJob\n\nA job is a plugin that can run periodically an do whatever you want. Typically, the kubernetes plugins of otoroshi are jobs that periodically sync stuff between otoroshi and kubernetes using the kube-api\n\n```rs\nuse extism_pdk::*;\n\n#[plugin_fn]\npub fn job_run(Json(_context): Json) -> FnResult> {\n ///\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobContext {\n pub attrs: Value,\n pub global_config: Value,\n pub snowflake: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WasmJobResult {\n\n}\n```\n\n### Common types\n\n```rs\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Backend {\n pub id: String,\n pub hostname: String,\n pub port: u32,\n pub tls: bool,\n pub weight: u32,\n pub protocol: String,\n pub ip_address: Option,\n pub predicate: Value,\n pub tls_config: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Apikey {\n #[serde(alias = \"clientId\")]\n pub client_id: String,\n #[serde(alias = \"clientName\")]\n pub client_name: String,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct User {\n pub name: String,\n pub email: String,\n pub profile: Value,\n pub metadata: HashMap,\n pub tags: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RawRequest {\n pub id: u32,\n pub method: String,\n pub headers: HashMap,\n pub cookies: Value,\n pub tls: bool,\n pub uri: String,\n pub path: String,\n pub version: String,\n pub has_body: bool,\n pub remote: String,\n pub client_cert_chain: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Frontend {\n pub domains: Vec,\n pub strict_path: Option,\n pub exact: bool,\n pub headers: HashMap,\n pub query: HashMap,\n pub methods: Vec,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct HealthCheck {\n pub enabled: bool,\n pub url: String,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct RouteBackend {\n pub targets: Vec,\n pub root: String,\n pub rewrite: bool,\n pub load_balancing: Value,\n pub client: Value,\n pub health_check: Option,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Route {\n pub id: String,\n pub name: String,\n pub description: String,\n pub tags: Vec,\n pub metadata: HashMap,\n pub enabled: bool,\n pub debug_flow: bool,\n pub export_reporting: bool,\n pub capture: bool,\n pub groups: Vec,\n pub frontend: Frontend,\n pub backend: RouteBackend,\n pub backend_ref: Option,\n pub plugins: Value,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct OtoroshiResponse {\n pub status: u32,\n pub headers: HashMap,\n pub cookies: Value,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct OtoroshiRequest {\n pub url: String,\n pub method: String,\n pub headers: HashMap,\n pub version: String,\n pub client_cert_chain: Value,\n pub backend: Option,\n pub cookies: Value,\n}\n```\n\n## Otoroshi interop. with host functions\n\notoroshi provides some host function in order make wasm interact with otoroshi internals. You can\n\n- access wasi resources\n- access http resources\n- access otoroshi internal state\n- access otoroshi internal configuration\n- access otoroshi static configuration\n- access plugin scoped in-memory key/value storage\n- access global in-memory key/value storage\n- access plugin scoped persistent key/value storage\n- access global persistent key/value storage\n\n### authorizations\n\nall the previously listed host functions are enabled with specific authorizations to avoid security issues with third party plugins. You can enable/disable the host function from the wasm plugin entity\n\n@@@ div { .centered-img }\n\n@@@\n\n\n### host functions abi\n\nyou'll find here the raw signatures for the otoroshi host functions. we are currently in the process of writing higher level functions to hide the complexity.\n\nevery time you the the following signature: `(context: u64, size: u64) -> u64` it means that otoroshi is expecting for a pointer to the call context (which is a json string) and it's size. The return is a pointer to the response (which is a json string).\n\nthe signature `(unused: u64) -> u64` means that there is no need for a params but as we technically need one (and hope to don't need one in the future), you have to pass something like `0` as parameter.\n\n```rust\nextern \"C\" {\n // log messages in otoroshi (log levels are 0 to 6 for trace, debug, info, warn, error, critical, max)\n fn proxy_log(logLevel: i32, message: u64, size: u64) -> i32;\n // trigger an otoroshi wasm event that can be exported through data exporters\n fn proxy_log_event(context: u64, size: u64) -> u64;\n // an http client\n fn proxy_http_call(context: u64, size: u64) -> u64;\n // access the current otoroshi state containing a snapshot of all otoroshi entities\n fn proxy_state(context: u64) -> u64;\n fn proxy_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi cluster configuration\n fn proxy_cluster_state(context: u64) -> u64;\n fn proxy_cluster_state_value(context: u64, size: u64) -> u64;\n // access the current otoroshi static configuration\n fn proxy_global_config(unused: u64) -> u64;\n // access the current otoroshi dynamic configuration\n fn proxy_config(unused: u64) -> u64;\n // access a persistent key/value store shared by every wasm plugins\n fn proxy_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_datastore_all_matching(context: u64, size: u64) -> u64;\n // access a persistent key/value store for the current plugin instance only\n fn proxy_plugin_datastore_keys(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_exists(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pttl(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_setnx(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_del(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_incrby(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_pexpire(context: u64, size: u64) -> u64;\n fn proxy_plugin_datastore_all_matching(context: u64, size: u64) -> u64;\n // access an in memory key/value store for the current plugin instance only\n fn proxy_plugin_map_set(context: u64, size: u64) -> u64;\n fn proxy_plugin_map_get(context: u64, size: u64) -> u64;\n fn proxy_plugin_map(unused: u64) -> u64;\n // access an in memory key/value store shared by every wasm plugins\n fn proxy_global_map_set(context: u64, size: u64) -> u64;\n fn proxy_global_map_get(context: u64, size: u64) -> u64;\n fn proxy_global_map(unused: u64) -> u64;\n}\n```\n\nright know, when using the Wasmo, a default idiomatic implementation is provided for `TinyGo` and `Rust`\n\nhost.rs\n: @@snip [host.rs](../snippets/wasmo/host.rs) \n\nhost.go\n: @@snip [host.go](../snippets/wasmo/host.go) \n"}] \ No newline at end of file diff --git a/manual/src/main/paradox/snippets/reference-env.conf b/manual/src/main/paradox/snippets/reference-env.conf index ed3b59f424..e69de29bb2 100644 --- a/manual/src/main/paradox/snippets/reference-env.conf +++ b/manual/src/main/paradox/snippets/reference-env.conf @@ -1,1056 +0,0 @@ -app { - storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql - storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql - storageRoot = ${?APP_STORAGE_ROOT} # the prefix used for storage keys - storageRoot = ${?OTOROSHI_STORAGE_ROOT} # the prefix used for storage keys - eventsName = ${?APP_EVENTS_NAME} # the name of the event producer - eventsName = ${?OTOROSHI_EVENTS_NAME} # the name of the event producer - importFrom = ${?APP_IMPORT_FROM} # file path to import otoroshi initial configuration - importFrom = ${?OTOROSHI_IMPORT_FROM} # file path to import otoroshi initial configuration - env = ${?APP_ENV} # env name, should always be prod except in dev mode - env = ${?OTOROSHI_ENV} # env name, should always be prod except in dev mode - domain = ${?APP_DOMAIN} # default domain for basic otoroshi services - domain = ${?OTOROSHI_DOMAIN} # default domain for basic otoroshi services - commitId = ${?COMMIT_ID} - commitId = ${?OTOROSHI_COMMIT_ID} - rootScheme = ${?APP_ROOT_SCHEME} # default root scheme when composing urls - rootScheme = ${?OTOROSHI_ROOT_SCHEME} # default root scheme when composing urls - throttlingWindow = ${?THROTTLING_WINDOW} # the number of second used to compute throttling number - throttlingWindow = ${?OTOROSHI_THROTTLING_WINDOW} # the number of second used to compute throttling number - checkForUpdates = ${?CHECK_FOR_UPDATES} # enable automatic version update checks - checkForUpdates = ${?OTOROSHI_CHECK_FOR_UPDATES} # enable automatic version update checks - overheadThreshold = ${?OVERHEAD_THRESHOLD} # the value threshold (in milliseconds) used to send HighOverheadAlert - overheadThreshold = ${?OTOROSHI_OVERHEAD_THRESHOLD} # the value threshold (in milliseconds) used to send HighOverheadAlert - adminLogin = ${?OTOROSHI_INITIAL_ADMIN_LOGIN} # the initial admin login - adminPassword = ${?OTOROSHI_INITIAL_ADMIN_PASSWORD} # the initial admin password - initialCustomization = ${?OTOROSHI_INITIAL_CUSTOMIZATION} # otoroshi inital configuration that will be merged with a new confguration. Shaped like an otoroshi export - boot { - failOnTimeout = ${?OTOROSHI_BOOT_FAIL_ON_TIMEOUT} # otoroshi will exit if a subsystem failed its init - globalWait = ${?OTOROSHI_BOOT_GLOBAL_WAIT} # should we wait until everything is setup to accept http requests - globalWaitTimeout = ${?OTOROSHI_BOOT_GLOBAL_WAIT_TIMEOUT} # max wait before accepting requests - waitForPluginsSearch = ${?OTOROSHI_BOOT_WAIT_FOR_PLUGINS_SEARCH} # should we wait for classpath plugins search before accepting http requests - waitForPluginsSearchTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_PLUGINS_SEARCH_TIMEOUT} # max wait for classpath plugins search before accepting http requests - waitForScriptsCompilation = ${?OTOROSHI_BOOT_WAIT_FOR_SCRIPTS_COMPILATION} # should we wait for plugins compilation before accepting http requests - waitForScriptsCompilationTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_SCRIPTS_COMPILATION_TIMEOUT} # max wait for plugins compilation before accepting http requests - waitForTlsInit = ${?OTOROSHI_BOOT_WAIT_FOR_TLS_INIT} # should we wait for first TLS context initialization before accepting http requests - waitForTlsInitTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_TLS_INIT_TIMEOUT} # max wait for first TLS context initialization before accepting http requests - waitForFirstClusterFetch = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_FETCH} # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterFetchTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_TIMEOUT} # max wait for first cluster initialization before accepting http requests - waitForFirstClusterStateCache = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_STATE_CACHE} # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterStateCacheTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_STATE_CACHE_TIMEOUT} # max wait for first cluster initialization before accepting http requests - } - instance { - instanceId = ${?OTOROSHI_INSTANCE_ID} # the instance id - number = ${?OTOROSHI_INSTANCE_NUMBER} # the instance number. Can be found in otoroshi events - number = ${?INSTANCE_NUMBER} # the instance number. Can be found in otoroshi events - name = ${?OTOROSHI_INSTANCE_NAME} # instance name - logo = ${?OTOROSHI_INSTANCE_LOGO} # instance logo - zone = ${?OTOROSHI_INSTANCE_ZONE} # instance zone (optional) - region = ${?OTOROSHI_INSTANCE_REGION} # instance region (optional) - dc = ${?OTOROSHI_INSTANCE_DATACENTER} # instance dc (optional) - provider = ${?OTOROSHI_INSTANCE_PROVIDER} # instance provider (optional) - rack = ${?OTOROSHI_INSTANCE_RACK} # instance rack (optional) - title = ${?OTOROSHI_INSTANCE_TITLE} # the title displayed in UI top left - } - longRequestTimeout = ${?OTOROSHI_PROXY_LONG_REQUEST_TIMEOUT} - } - health { - limit = ${?HEALTH_LIMIT} # the value threshold (in milliseconds) used to indicate if an otoroshi instance is healthy or not - limit = ${?OTOROSHI_HEALTH_LIMIT} # the value threshold (in milliseconds) used to indicate if an otoroshi instance is healthy or not - accessKey = ${?HEALTH_ACCESS_KEY} # the key to access /health edpoint - accessKey = ${?OTOROSHI_HEALTH_ACCESS_KEY} # the key to access /health edpoint - } - snowflake { - seed = ${?INSTANCE_NUMBER} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?OTOROSHI_INSTANCE_NUMBER} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?SNOWFLAKE_SEED} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?OTOROSHI_SNOWFLAKE_SEED} # the seed number used to generate unique ids. Should be different for every instances - } - events { - maxSize = ${?MAX_EVENTS_SIZE} # the amount of event kept in the datastore - maxSize = ${?OTOROSHI_MAX_EVENTS_SIZE} # the amount of event kept in the datastore - } - exposed-ports { - http = ${?APP_EXPOSED_PORTS_HTTP} # the exposed http port for otoroshi (when in a container or behind a proxy) - http = ${?OTOROSHI_EXPOSED_PORTS_HTTP} # the exposed http port for otoroshi (when in a container or behind a proxy) - https = ${?APP_EXPOSED_PORTS_HTTPS} # the exposed https port for otoroshi (when in a container or behind a proxy - https = ${?OTOROSHI_EXPOSED_PORTS_HTTPS} # the exposed https port for otoroshi (when in a container or behind a proxy - } - backoffice { - exposed = ${?APP_BACKOFFICE_EXPOSED} # expose the backoffice ui - exposed = ${?OTOROSHI_BACKOFFICE_EXPOSED} # expose the backoffice ui - subdomain = ${?APP_BACKOFFICE_SUBDOMAIN} # the backoffice subdomain - subdomain = ${?OTOROSHI_BACKOFFICE_SUBDOMAIN} # the backoffice subdomain - domainsStr = ${?APP_BACKOFFICE_DOMAINS} # the backoffice domains - domainsStr = ${?OTOROSHI_BACKOFFICE_DOMAINS} # the backoffice domains - # useNewEngine = ${?OTOROSHI_BACKOFFICE_USE_NEW_ENGINE} # avoid backoffice admin api proxy - usePlay = ${?OTOROSHI_BACKOFFICE_USE_PLAY} # avoid backoffice http call for admin api - session { - exp = ${?APP_BACKOFFICE_SESSION_EXP} # the backoffice cookie expiration - exp = ${?OTOROSHI_BACKOFFICE_SESSION_EXP} # the backoffice cookie expiration - } - } - privateapps { - subdomain = ${?APP_PRIVATEAPPS_SUBDOMAIN} # privateapps (proxy sso) domain - subdomain = ${?OTOROSHI_PRIVATEAPPS_SUBDOMAIN} # privateapps (proxy sso) domain - domainsStr = ${?APP_PRIVATEAPPS_DOMAINS} - domainsStr = ${?OTOROSHI_PRIVATEAPPS_DOMAINS} - session { - exp = ${?APP_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration - exp = ${?OTOROSHI_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration - } - } - adminapi { - exposed = ${?ADMIN_API_EXPOSED} # expose the admin api - exposed = ${?OTOROSHI_ADMIN_API_EXPOSED} # expose the admin api - targetSubdomain = ${?ADMIN_API_TARGET_SUBDOMAIN} # admin api target subdomain as targeted by otoroshi service - targetSubdomain = ${?OTOROSHI_ADMIN_API_TARGET_SUBDOMAIN} # admin api target subdomain as targeted by otoroshi service - exposedSubdomain = ${?ADMIN_API_EXPOSED_SUBDOMAIN} # admin api exposed subdomain as exposed by otoroshi service - exposedSubdomain = ${?OTOROSHI_ADMIN_API_EXPOSED_SUBDOMAIN} # admin api exposed subdomain as exposed by otoroshi service - additionalExposedDomain = ${?ADMIN_API_ADDITIONAL_EXPOSED_DOMAIN} # admin api additional exposed subdomain as exposed by otoroshi service - additionalExposedDomain = ${?OTOROSHI_ADMIN_API_ADDITIONAL_EXPOSED_DOMAIN} # admin api additional exposed subdomain as exposed by otoroshi service - domainsStr = ${?ADMIN_API_DOMAINS} - domainsStr = ${?OTOROSHI_ADMIN_API_DOMAINS} - exposedDomainsStr = ${?ADMIN_API_EXPOSED_DOMAINS} - exposedDomainsStr = ${?OTOROSHI_ADMIN_API_EXPOSED_DOMAINS} - defaultValues { - backOfficeGroupId = ${?ADMIN_API_GROUP} # default value for admin api service group - backOfficeGroupId = ${?OTOROSHI_ADMIN_API_GROUP} # default value for admin api service group - backOfficeApiKeyClientId = ${?ADMIN_API_CLIENT_ID} # default value for admin api apikey id - backOfficeApiKeyClientId = ${?OTOROSHI_ADMIN_API_CLIENT_ID} # default value for admin api apikey id - backOfficeApiKeyClientSecret = ${?otoroshi.admin-api-secret} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?OTOROSHI_otoroshi.admin-api-secret} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?ADMIN_API_CLIENT_SECRET} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?OTOROSHI_ADMIN_API_CLIENT_SECRET} # default value for admin api apikey secret - backOfficeServiceId = ${?ADMIN_API_SERVICE_ID} # default value for admin api service id - backOfficeServiceId = ${?OTOROSHI_ADMIN_API_SERVICE_ID} # default value for admin api service id - } - proxy { - https = ${?ADMIN_API_HTTPS} # backoffice proxy admin api over https - https = ${?OTOROSHI_ADMIN_API_HTTPS} # backoffice proxy admin api over https - local = ${?ADMIN_API_LOCAL} # backoffice proxy admin api on localhost - local = ${?OTOROSHI_ADMIN_API_LOCAL} # backoffice proxy admin api on localhost - } - } - claim { - sharedKey = ${?CLAIM_SHAREDKEY} # the default secret used to sign otoroshi exchange protocol tokens - sharedKey = ${?OTOROSHI_CLAIM_SHAREDKEY} # the default secret used to sign otoroshi exchange protocol tokens - } - webhooks { - } - redis { # configuration to fetch/store otoroshi state from a redis datastore using rediscala - host = ${?REDIS_HOST} - host = ${?OTOROSHI_REDIS_HOST} - port = ${?REDIS_PORT} - port = ${?OTOROSHI_REDIS_PORT} - password = ${?REDIS_PASSWORD} - password = ${?OTOROSHI_REDIS_PASSWORD} - windowSize = ${?REDIS_WINDOW_SIZE} - windowSize = ${?OTOROSHI_REDIS_WINDOW_SIZE} - slavesStr = ${?REDIS_SLAVES} - slavesStr = ${?OTOROSHI_REDIS_SLAVES} - slavesStr = ${?REDIS_MEMBERS} - slavesStr = ${?OTOROSHI_REDIS_MEMBERS} - useScan = ${?REDIS_USE_SCAN} - useScan = ${?OTOROSHI_REDIS_USE_SCAN} - pool { - members = ${?REDIS_POOL_MEMBERS} - members = ${?OTOROSHI_REDIS_POOL_MEMBERS} - } - mpool { - membersStr = ${?REDIS_MPOOL_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_MPOOL_MEMBERS} - } - lf { - master { - host = ${?REDIS_LF_HOST} - host = ${?OTOROSHI_REDIS_LF_HOST} - port = ${?REDIS_LF_PORT} - port = ${?OTOROSHI_REDIS_LF_PORT} - password = ${?REDIS_LF_PASSWORD} - password = ${?OTOROSHI_REDIS_LF_PASSWORD} - } - slavesStr = ${?REDIS_LF_SLAVES} - slavesStr = ${?OTOROSHI_REDIS_LF_SLAVES} - slavesStr = ${?REDIS_LF_MEMBERS} - slavesStr = ${?OTOROSHI_REDIS_LF_MEMBERS} - } - sentinels { - master = ${?REDIS_SENTINELS_MASTER} - master = ${?OTOROSHI_REDIS_SENTINELS_MASTER} - password = ${?REDIS_SENTINELS_PASSWORD} - password = ${?OTOROSHI_REDIS_SENTINELS_PASSWORD} - db = ${?REDIS_SENTINELS_DB} - db = ${?OTOROSHI_REDIS_SENTINELS_DB} - name = ${?REDIS_SENTINELS_NAME} - name = ${?OTOROSHI_REDIS_SENTINELS_NAME} - membersStr = ${?REDIS_SENTINELS_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_SENTINELS_MEMBERS} - lf { - master = ${?REDIS_SENTINELS_LF_MASTER} - master = ${?OTOROSHI_REDIS_SENTINELS_LF_MASTER} - membersStr = ${?REDIS_SENTINELS_LF_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_SENTINELS_LF_MEMBERS} - } - } - cluster { - membersStr = ${?REDIS_CLUSTER_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_CLUSTER_MEMBERS} - } - lettuce { # configuration to fetch/store otoroshi state from a redis datastore using the lettuce driver (the next default one) - connection = ${?REDIS_LETTUCE_CONNECTION} - connection = ${?OTOROSHI_REDIS_LETTUCE_CONNECTION} - uri = ${?REDIS_LETTUCE_URI} - uri = ${?OTOROSHI_REDIS_LETTUCE_URI} - uri = ${?REDIS_URL} - uri = ${?OTOROSHI_REDIS_URL} - urisStr = ${?REDIS_LETTUCE_URIS} - urisStr = ${?OTOROSHI_REDIS_LETTUCE_URIS} - readFrom = ${?REDIS_LETTUCE_READ_FROM} - readFrom = ${?OTOROSHI_REDIS_LETTUCE_READ_FROM} - startTLS = ${?REDIS_LETTUCE_START_TLS} - startTLS = ${?OTOROSHI_REDIS_LETTUCE_START_TLS} - verifyPeers = ${?REDIS_LETTUCE_VERIFY_PEERS} - verifyPeers = ${?OTOROSHI_REDIS_LETTUCE_VERIFY_PEERS} - } - } - inmemory { # configuration to fetch/store otoroshi state in memory - windowSize = ${?INMEMORY_WINDOW_SIZE} - windowSize = ${?OTOROSHI_INMEMORY_WINDOW_SIZE} - experimental = ${?INMEMORY_EXPERIMENTAL_STORE} - experimental = ${?OTOROSHI_INMEMORY_EXPERIMENTAL_STORE} - optimized = ${?INMEMORY_OPTIMIZED} - optimized = ${?OTOROSHI_INMEMORY_OPTIMIZED} - modern = ${?INMEMORY_MODERN} - modern = ${?OTOROSHI_INMEMORY_MODERN} - } - filedb { # configuration to fetch/store otoroshi state from a file - windowSize = ${?FILEDB_WINDOW_SIZE} - windowSize = ${?OTOROSHI_FILEDB_WINDOW_SIZE} - path = ${?FILEDB_PATH} - path = ${?OTOROSHI_FILEDB_PATH} - } - httpdb { # configuration to fetch/store otoroshi state from an http endpoint - headers = {} - } - s3db { # configuration to fetch/store otoroshi state from a S3 bucket - bucket = ${?OTOROSHI_DB_S3_BUCKET} - endpoint = ${?OTOROSHI_DB_S3_ENDPOINT} - region = ${?OTOROSHI_DB_S3_REGION} - access = ${?OTOROSHI_DB_S3_ACCESS} - secret = ${?OTOROSHI_DB_S3_SECRET} - key = ${?OTOROSHI_DB_S3_KEY} - chunkSize = ${?OTOROSHI_DB_S3_CHUNK_SIZE} - v4auth = ${?OTOROSHI_DB_S3_V4_AUTH} - writeEvery = ${?OTOROSHI_DB_S3_WRITE_EVERY} # write interval - acl = ${?OTOROSHI_DB_S3_ACL} - } - pg { # postrgesql settings. everything possible with the client - uri = ${?PG_URI} - uri = ${?OTOROSHI_PG_URI} - uri = ${?POSTGRESQL_ADDON_URI} - uri = ${?OTOROSHI_POSTGRESQL_ADDON_URI} - poolSize = ${?PG_POOL_SIZE} - poolSize = ${?OTOROSHI_PG_POOL_SIZE} - port = ${?PG_PORT} - port = ${?OTOROSHI_PG_PORT} - host = ${?PG_HOST} - host = ${?OTOROSHI_PG_HOST} - database = ${?PG_DATABASE} - database = ${?OTOROSHI_PG_DATABASE} - user = ${?PG_USER} - user = ${?OTOROSHI_PG_USER} - password = ${?PG_PASSWORD} - password = ${?OTOROSHI_PG_PASSWORD} - logQueries = ${?PG_DEBUG_QUERIES} - logQueries = ${?OTOROSHI_PG_DEBUG_QUERIES} - avoidJsonPath = ${?PG_AVOID_JSON_PATH} - avoidJsonPath = ${?OTOROSHI_PG_AVOID_JSON_PATH} - optimized = ${?PG_OPTIMIZED} - optimized = ${?OTOROSHI_PG_OPTIMIZED} - connect-timeout = ${?PG_CONNECT_TIMEOUT} - connect-timeout = ${?OTOROSHI_PG_CONNECT_TIMEOUT} - idle-timeout = ${?PG_IDLE_TIMEOUT} - idle-timeout = ${?OTOROSHI_PG_IDLE_TIMEOUT} - log-activity = ${?PG_LOG_ACTIVITY} - log-activity = ${?OTOROSHI_PG_LOG_ACTIVITY} - pipelining-limit = ${?PG_PIPELINING_LIMIT} - pipelining-limit = ${?OTOROSHI_PG_PIPELINING_LIMIT} - ssl { - enabled = ${?PG_SSL_ENABLED} - enabled = ${?OTOROSHI_PG_SSL_ENABLED} - mode = ${?PG_SSL_MODE} - mode = ${?OTOROSHI_PG_SSL_MODE} - trusted-cert-path = ${?PG_SSL_TRUSTED_CERT_PATH} - trusted-cert-path = ${?OTOROSHI_PG_SSL_TRUSTED_CERT_PATH} - trusted-cert = ${?PG_SSL_TRUSTED_CERT} - trusted-cert = ${?OTOROSHI_PG_SSL_TRUSTED_CERT} - client-cert-path = ${?PG_SSL_CLIENT_CERT_PATH} - client-cert-path = ${?OTOROSHI_PG_SSL_CLIENT_CERT_PATH} - client-cert = ${?PG_SSL_CLIENT_CERT} - client-cert = ${?OTOROSHI_PG_SSL_CLIENT_CERT} - trust-all = ${?PG_SSL_TRUST_ALL} - trust-all = ${?OTOROSHI_PG_SSL_TRUST_ALL} - } - } - cassandra { # cassandra settings. everything possible with the client - windowSize = ${?CASSANDRA_WINDOW_SIZE} - windowSize = ${?OTOROSHI_CASSANDRA_WINDOW_SIZE} - host = ${?CASSANDRA_HOST} - host = ${?OTOROSHI_CASSANDRA_HOST} - port = ${?CASSANDRA_PORT} - port = ${?OTOROSHI_CASSANDRA_PORT} - replicationFactor = ${?CASSANDRA_REPLICATION_FACTOR} - replicationFactor = ${?OTOROSHI_CASSANDRA_REPLICATION_FACTOR} - replicationOptions = ${?CASSANDRA_REPLICATION_OPTIONS} - replicationOptions = ${?OTOROSHI_CASSANDRA_REPLICATION_OPTIONS} - durableWrites = ${?CASSANDRA_DURABLE_WRITES} - durableWrites = ${?OTOROSHI_CASSANDRA_DURABLE_WRITES} - basic.contact-points = [ ${app.cassandra.host}":"${app.cassandra.port} ] - basic.session-name = ${?OTOROSHI_CASSANDRA_SESSION_NAME} - basic.session-keyspace = ${?OTOROSHI_CASSANDRA_SESSION_KEYSPACE} - basic.request { - consistency = ${?OTOROSHI_CASSANDRA_CONSISTENCY} - page-size = ${?OTOROSHI_CASSANDRA_PAGE_SIZE} - serial-consistency = ${?OTOROSHI_CASSANDRA_SERIAL_CONSISTENCY} - default-idempotence = ${?OTOROSHI_CASSANDRA_DEFAULT_IDEMPOTENCE} - } - basic.load-balancing-policy { - local-datacenter = ${?OTOROSHI_CASSANDRA_LOCAL_DATACENTER} - } - basic.cloud { - } - basic.application { - } - basic.graph { - } - advanced.connection { - set-keyspace-timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - pool { - local { - } - remote { - } - } - } - advanced.reconnection-policy { - } - advanced.retry-policy { - } - advanced.speculative-execution-policy { - } - advanced.auth-provider { - username = ${?CASSANDRA_USERNAME} - username = ${?OTOROSHI_CASSANDRA_USERNAME} - password = ${?CASSANDRA_PASSWORD} - password = ${?OTOROSHI_CASSANDRA_PASSWORD} - authorization-id = ${?OTOROSHI_CASSANDRA_AUTHORIZATION_ID} - # login-configuration { - # } - # sasl-properties { - # } - } - advanced.ssl-engine-factory { - } - advanced.timestamp-generator { - drift-warning { - } - } - advanced.request-tracker { - logs { - slow { - } - } - } - advanced.throttler { - } - advanced.address-translator { - } - advanced.protocol { - version = ${?OTOROSHI_CASSANDRA_PROTOCOL_VERSION} - compression = ${?OTOROSHI_CASSANDRA_PROTOCOL_COMPRESSION} - } - advanced.request { - trace { - } - } - advanced.graph { - paging-options { - page-size = ${datastax-java-driver.advanced.continuous-paging.page-size} - max-pages = ${datastax-java-driver.advanced.continuous-paging.max-pages} - max-pages-per-second = ${datastax-java-driver.advanced.continuous-paging.max-pages-per-second} - max-enqueued-pages = ${datastax-java-driver.advanced.continuous-paging.max-enqueued-pages} - } - } - advanced.continuous-paging { - page-size = ${datastax-java-driver.basic.request.page-size} - timeout { - } - } - advanced.monitor-reporting { - } - advanced.metrics { - session { - cql-requests { - } - throttling.delay { - } - continuous-cql-requests { - } - graph-requests { - } - } - node { - cql-messages { - } - graph-messages { - } - } - } - advanced.socket { - } - advanced.heartbeat { - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - } - advanced.metadata { - topology-event-debouncer { - } - schema { - request-timeout = ${datastax-java-driver.basic.request.timeout} - request-page-size = ${datastax-java-driver.basic.request.page-size} - debouncer { - } - } - } - advanced.control-connection { - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - schema-agreement { - } - } - advanced.prepared-statements { - reprepare-on-up { - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - } - } - advanced.netty { - io-group { - shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} - } - admin-group { - shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} - } - timer { - } - } - advanced.coalescer { - } - } - actorsystems { - otoroshi { - akka { # otoroshi actorsystem configuration - version = ${akka.version} - default-dispatcher { - fork-join-executor { - parallelism-factor = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_FACTOR} - parallelism-min = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_MIN} - parallelism-max = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_MAX} - task-peeking-mode = ${?OTOROSHI_CORE_DISPATCHER_TASK_PEEKING_MODE} - } - throughput = ${?OTOROSHI_CORE_DISPATCHER_THROUGHPUT} - } - http { - parsing { - max-uri-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_URI_LENGTH} - max-method-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_METHOD_LENGTH} - max-response-reason-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_RESPONSE_REASON_LENGTH} - max-header-name-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_NAME_LENGTH} - max-header-value-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_VALUE_LENGTH} - max-header-count = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_COUNT} - max-chunk-ext-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CHUNK_EXT_LENGTH} - max-chunk-size = ${?AKKA_HTTP_CLIENT_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CHUNK_SIZE} - max-content-length = ${?AKKA_HTTP_CLIENT_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CONTENT_LENGHT} - max-to-strict-bytes = ${?AKKA_HTTP_CLIENT_MAX_TO_STRICT_BYTES} - max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_TO_STRICT_BYTES} - max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_TO_STRICT_BYTES} - } - } - } - } - datastore { - akka { - version = ${akka.version} - default-dispatcher { - fork-join-executor { - } - } - } - } - } -} -otoroshi { - domain = ${?app.domain} - maintenanceMode = ${?OTOROSHI_MAINTENANCE_MODE_ENABLED} # enable global maintenance mode - secret = ${?OTOROSHI_SECRET} # the secret used to sign sessions - admin-api-secret = ${?OTOROSHI_ADMIN_API_SECRET} # the secret for admin api - elSettings { - allowEnvAccess = ${?OTOROSHI_EL_SETTINGS_ALLOW_ENV_ACCESS} - allowConfigAccess = ${?OTOROSHI_EL_SETTINGS_ALLOW_CONFIG_ACCESS} - } - open-telemetry { - server-logs { - enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED} - gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP} - grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC} - endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT} - timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT} - client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT} - trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT} - headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS} - max_batch = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_BATCH} - max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION} - } - server-metrics { - enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED} - gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP} - grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC} - endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT} - timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT} - client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT} - trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT} - headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS} - max_batch = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_BATCH} - max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION} - } - } - next { - state-sync-interval = ${?OTOROSHI_NEXT_STATE_SYNC_INTERVAL} - export-reporting = ${?OTOROSHI_NEXT_EXPORT_REPORTING} - monitor-proxy-state-size = ${?OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE} - monitor-datastore-size = ${?OTOROSHI_NEXT_MONITOR_DATASTORE_SIZE} - plugins { - merge-sync-steps = ${?OTOROSHI_NEXT_PLUGINS_MERGE_SYNC_STEPS} - apply-legacy-checks = ${?OTOROSHI_NEXT_PLUGINS_APPLY_LEGACY_CHECKS} - } - experimental { - netty-client { - wiretap = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_WIRETAP} - enforce = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_ENFORCE} - enforce-akka = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_ENFORCE_AKKA} - } - netty-server { - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED} - new-engine-only = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY} - host = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST} - http-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT} - exposed-http-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_EXPOSED_HTTP_PORT} - https-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT} - exposed-https-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_EXPOSED_HTTPS_PORT} - wiretap = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP} - accesslog = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG} - threads = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS} - parser { - allowDuplicateContentLengths = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS} - validateHeaders = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS} - h2cMaxContentLength = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH} - initialBufferSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE} - maxHeaderSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE} - maxInitialLineLength = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH} - maxChunkSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE} - } - http2 { - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED} - h2c = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C} - } - http3 { - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED} - port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT} - exposedPort = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_EXPOSED_PORT} - initialMaxStreamsBidirectional = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL} - initialMaxStreamDataBidirectionalRemote = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE} - initialMaxStreamDataBidirectionalLocal = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL} - initialMaxData = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA} - maxRecvUdpPayloadSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE} - maxSendUdpPayloadSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE} - disableQpackDynamicTable = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_DISABLE_QPACK_DYNAMIC_TABLE} - } - native { - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED} - driver = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER} - } - } - } - } - options { - bypassUserRightsCheck = ${?OTOROSHI_OPTIONS_BYPASSUSERRIGHTSCHECK} - emptyContentLengthIsChunked = ${?OTOROSHI_OPTIONS_EMPTYCONTENTLENGTHISCHUNKED} - detectApiKeySooner = ${?OTOROSHI_OPTIONS_DETECTAPIKEYSOONER} - sendClientChainAsPem = ${?OTOROSHI_OPTIONS_SENDCLIENTCHAINASPEM} - useOldHeadersComposition = ${?OTOROSHI_OPTIONS_USEOLDHEADERSCOMPOSITION} - manualDnsResolve = ${?OTOROSHI_OPTIONS_MANUALDNSRESOLVE} - useEventStreamForScriptEvents = ${?OTOROSHI_OPTIONS_USEEVENTSTREAMFORSCRIPTEVENTS} - trustXForwarded = ${?OTOROSHI_OPTIONS_TRUST_XFORWARDED} - disableFunnyLogos = ${?OTOROSHI_OPTIONS_DISABLE_FUNNY_LOGOS} - staticExposedDomain = ${?OTOROSHI_OPTIONS_STATIC_EXPOSED_DOMAIN} - enable-json-media-type-with-open-charset = ${?OTOROSHI_OPTIONS_ENABLE_JSON_MEDIA_TYPE_WITH_OPEN_CHARSET} - dynamicBodySizeCompute = ${?OTOROSHI_OPTIONS_DYNAMIC_BODY_SIZE_COMPUTE} - } - wasm { - cache { - ttl = ${?OTOROSHI_WASM_CACHE_TTL} - size = ${?OTOROSHI_WASM_CACHE_SIZE} - } - queue { - buffer { - size = ${?OTOROSHI_WASM_QUEUE_BUFFER_SIZE} - } - } - } - anonymous-reporting { - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_ENABLED} - url = ${?OTOROSHI_ANONYMOUS_REPORTING_REDIRECT} - url = ${?OTOROSHI_ANONYMOUS_REPORTING_URL} - timeout = ${?OTOROSHI_ANONYMOUS_REPORTING_TIMEOUT} - tls { - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_ENABLED} # enable mtls - loose = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_LOOSE} # loose verification - trustAll = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_ALL} # trust any CA - } - proxy { - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_ENABLED} # enable proxy - host = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_HOST}, - port = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PORT}, - principal = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PRINCIPAL}, - password = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PASSWORD}, - ntlmDomain = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_DOMAIN}, - encoding = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_ENCODING}, - } - } - backoffice { - flags { - useAkkaHttpClient = ${?OTOROSHI_BACKOFFICE_FLAGS_USE_AKKA_HTTP_CLIENT} - logUrl = ${?OTOROSHI_BACKOFFICE_FLAGS_LOG_URL} - requestTimeout = ${?OTOROSHI_BACKOFFICE_FLAGS_REQUEST_TIMEOUT} - } - } - sessions { - secret = ${otoroshi.secret} - secret = ${?OTOROSHI_SESSIONS_SECRET} - } - cache { - enabled = ${?USE_CACHE} - enabled = ${?OTOROSHI_USE_CACHE} - enabled = ${?OTOROSHI_ENTITIES_CACHE_ENABLED} - ttl = ${?OTOROSHI_ENTITIES_CACHE_TTL} - } - metrics { - enabled = ${?OTOROSHI_METRICS_ENABLED} - every = ${?OTOROSHI_METRICS_EVERY} - accessKey = ${?app.health.accessKey} - accessKey = ${?OTOROSHI_app.health.accessKey} - accessKey = ${?OTOROSHI_METRICS_ACCESS_KEY} - } - plugins { - packagesStr = ${?OTOROSHI_PLUGINS_SCAN_PACKAGES} - print = ${?OTOROSHI_PLUGINS_PRINT} - blacklistedStr = ${?OTOROSHI_PLUGINS_BACKLISTED} - } - scripts { - enabled = ${?OTOROSHI_SCRIPTS_ENABLED} # enable scripts - static { # settings for statically enabled script/plugins - enabled = ${?OTOROSHI_SCRIPTS_STATIC_ENABLED} - transformersRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_TRANSFORMER_REFS} - transformersConfig = {} - transformersConfigStr= ${?OTOROSHI_SCRIPTS_STATIC_TRANSFORMER_CONFIG} - validatorRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_VALIDATOR_REFS} - validatorConfig = {} - validatorConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_VALIDATOR_CONFIG} - preRouteRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_PRE_ROUTE_REFS} - preRouteConfig = {} - preRouteConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_PRE_ROUTE_CONFIG} - sinkRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_SINK_REFS} - sinkConfig = {} - sinkConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_SINK_CONFIG} - jobsRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_JOBS_REFS} - jobsConfig = {} - jobsConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_JOBS_CONFIG} - } - } - tls = ${otoroshi.ssl} - ssl { - cipherSuites = ${otoroshi.ssl.cipherSuitesJDK11} - protocols = ${otoroshi.ssl.modernProtocols} - cacert { - } - fromOutside { - clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH} - clientAuth = ${?OTOROSHI_SSL_OUTSIDE_CLIENT_AUTH} - netty { - clientAuth = ${?OTOROSHI_SSL_OUTSIDE_NETTY_CLIENT_AUTH} - } - } - trust { - all = ${?OTOROSHI_SSL_TRUST_ALL} - } - rootCa { - ca = ${?OTOROSHI_SSL_ROOTCA_CA} - cert = ${?OTOROSHI_SSL_ROOTCA_CERT} - key = ${?OTOROSHI_SSL_ROOTCA_KEY} - importCa = ${?OTOROSHI_SSL_ROOTCA_IMPORTCA} - } - initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT} - initialCacert = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CACERT} - initialCacert = ${?INITIAL_CACERT} - initialCacert = ${?OTOROSHI_INITIAL_CACERT} - initialCert = ${?CLUSTER_WORKER_INITIAL_CERT} - initialCert = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CERT} - initialCert = ${?INITIAL_CERT} - initialCert = ${?OTOROSHI_INITIAL_CERT} - initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY} - initialCertKey = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CERT_KEY} - initialCertKey = ${?INITIAL_CERT_KEY} - initialCertKey = ${?OTOROSHI_INITIAL_CERT_KEY} - initialCertImportCa = ${?OTOROSHI_INITIAL_CERT_IMPORTCA} - } - cluster { - mode = ${?CLUSTER_MODE} # can be "off", "leader", "worker" - mode = ${?OTOROSHI_CLUSTER_MODE} # can be "off", "leader", "worker" - compression = ${?CLUSTER_COMPRESSION} # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9 - compression = ${?OTOROSHI_CLUSTER_COMPRESSION} # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9 - retryDelay = ${?CLUSTER_RETRY_DELAY} # the delay before retrying a request to leader - retryDelay = ${?OTOROSHI_CLUSTER_RETRY_DELAY} # the delay before retrying a request to leader - retryFactor = ${?CLUSTER_RETRY_FACTOR} # the retry factor to avoid high load on failing nodes - retryFactor = ${?OTOROSHI_CLUSTER_RETRY_FACTOR} # the retry factor to avoid high load on failing nodes - selfAddress = ${?CLUSTER_SELF_ADDRESS} # the instance ip address - selfAddress = ${?OTOROSHI_CLUSTER_SELF_ADDRESS} # the instance ip address - autoUpdateState = ${?CLUSTER_AUTO_UPDATE_STATE} # auto update cluster state with a job (more efficient - autoUpdateState = ${?OTOROSHI_CLUSTER_AUTO_UPDATE_STATE} # auto update cluster state with a job (more efficient - backup { - enabled = ${?OTOROSHI_CLUSTER_BACKUP_ENABLED} - kind = ${?OTOROSHI_CLUSTER_BACKUP_KIND} - instance { - can-write = ${?OTOROSHI_CLUSTER_BACKUP_INSTANCE_CAN_WRITE} - can-read = ${?OTOROSHI_CLUSTER_BACKUP_INSTANCE_CAN_READ} - } - s3 { - bucket = ${?OTOROSHI_CLUSTER_BACKUP_S3_BUCKET} - endpoint = ${?OTOROSHI_CLUSTER_BACKUP_S3_ENDPOINT} - region = ${?OTOROSHI_CLUSTER_BACKUP_S3_REGION} - access = ${?OTOROSHI_CLUSTER_BACKUP_S3_ACCESSKEY} - secret = ${?OTOROSHI_CLUSTER_BACKUP_S3_SECRET} - path = ${?OTOROSHI_CLUSTER_BACKUP_S3_PATH} - chunk-size = ${?OTOROSHI_CLUSTER_BACKUP_S3_CHUNK_SIZE} - v4auth = ${?OTOROSHI_CLUSTER_BACKUP_S3_V4AUTH} - acl = ${?OTOROSHI_CLUSTER_BACKUP_S3_ACL} - } - } - relay { # relay routing settings - enabled = ${?OTOROSHI_CLUSTER_RELAY_ENABLED} # enable relay routing - leaderOnly = ${?OTOROSHI_CLUSTER_RELAY_LEADER_ONLY} # workers always pass through leader for relay routing - location { - provider = ${?otoroshi.instance.provider} - provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER} - provider = ${?app.instance.provider} - zone = ${?otoroshi.instance.zone} - zone = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_ZONE} - zone = ${?app.instance.zone} - region = ${?otoroshi.instance.region} - region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION} - region = ${?app.instance.region} - datacenter = ${?otoroshi.instance.dc} - datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER} - datacenter = ${?app.instance.dc} - rack = ${?otoroshi.instance.rack} - rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK} - rack = ${?app.instance.rack} - } - exposition { - url = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_URL} - urlsStr = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_URLS} - hostname = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_HOSTNAME} - clientId = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_CLIENT_ID} - clientSecret = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_CLIENT_SECRET} - ipAddress = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_IP_ADDRESS} - } - } - mtls { - enabled = ${?CLUSTER_MTLS_ENABLED} # enable mtls - enabled = ${?OTOROSHI_CLUSTER_MTLS_ENABLED} # enable mtls - loose = ${?CLUSTER_MTLS_LOOSE} # loose verification - loose = ${?OTOROSHI_CLUSTER_MTLS_LOOSE} # loose verification - trustAll = ${?CLUSTER_MTLS_TRUST_ALL} # trust any CA - trustAll = ${?OTOROSHI_CLUSTER_MTLS_TRUST_ALL} # trust any CA - } - proxy { - enabled = ${?CLUSTER_PROXY_ENABLED} # enable proxy - host = ${?CLUSTER_PROXY_HOST}, - port = ${?CLUSTER_PROXY_PORT}, - principal = ${?CLUSTER_PROXY_PRINCIPAL}, - password = ${?CLUSTER_PROXY_PASSWORD}, - ntlmDomain = ${?CLUSTER_PROXY_NTLM_DOMAIN}, - encoding = ${?CLUSTER_PROXY_ENCODING}, - } - leader { - name = ${?CLUSTER_LEADER_NAME} # the leader name - name = ${?OTOROSHI_CLUSTER_LEADER_NAME} # the leader name - urlsStr = ${?CLUSTER_LEADER_URLS} # the leader urls - urlsStr = ${?OTOROSHI_CLUSTER_LEADER_URLS} # the leader urls - url = ${?CLUSTER_LEADER_URL} # the leader url - url = ${?OTOROSHI_CLUSTER_LEADER_URL} # the leader url - host = ${?CLUSTER_LEADER_HOST} # the leaders api hostname - host = ${?OTOROSHI_CLUSTER_LEADER_HOST} # the leaders api hostname - clientId = ${?CLUSTER_LEADER_CLIENT_ID} # the leaders apikey id to access otoroshi admin api - clientId = ${?OTOROSHI_CLUSTER_LEADER_CLIENT_ID} # the leaders apikey id to access otoroshi admin api - clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET} # the leaders apikey secret to access otoroshi admin api - clientSecret = ${?OTOROSHI_CLUSTER_LEADER_CLIENT_SECRET} # the leaders apikey secret to access otoroshi admin api - groupingBy = ${?CLUSTER_LEADER_GROUP_BY} # items grouping when streaming state - groupingBy = ${?OTOROSHI_CLUSTER_LEADER_GROUP_BY} # items grouping when streaming state - cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR} # the ttl for local state cache - cacheStateFor = ${?OTOROSHI_CLUSTER_LEADER_CACHE_STATE_FOR} # the ttl for local state cache - stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH} # eventually a dump state path for debugging purpose - stateDumpPath = ${?OTOROSHI_CLUSTER_LEADER_DUMP_PATH} # eventually a dump state path for debugging purpose - } - worker { - name = ${?CLUSTER_WORKER_NAME} # the workers name - name = ${?OTOROSHI_CLUSTER_WORKER_NAME} # the workers name - retries = ${?CLUSTER_WORKER_RETRIES} # the number of retries when pushing quotas/pulling state - retries = ${?OTOROSHI_CLUSTER_WORKER_RETRIES} # the number of retries when pushing quotas/pulling state - timeout = ${?CLUSTER_WORKER_TIMEOUT} # the workers timeout when interacting with leaders - timeout = ${?OTOROSHI_CLUSTER_WORKER_TIMEOUT} # the workers timeout when interacting with leaders - tenantsStr = ${?CLUSTER_WORKER_TENANTS} # the list (coma separated) of organization served by this worker. If none, it's all - tenantsStr = ${?OTOROSHI_CLUSTER_WORKER_TENANTS} # the list (coma separated) of organization served by this worker. If none, it's all - dbpath = ${?CLUSTER_WORKER_DB_PATH} # state dump path for debugging purpose - dbpath = ${?OTOROSHI_CLUSTER_WORKER_DB_PATH} # state dump path for debugging purpose - dataStaleAfter = ${?CLUSTER_WORKER_DATA_STALE_AFTER} # the amount of time needed to consider state is stale - dataStaleAfter = ${?OTOROSHI_CLUSTER_WORKER_DATA_STALE_AFTER} # the amount of time needed to consider state is stale - swapStrategy = ${?CLUSTER_WORKER_SWAP_STRATEGY} # the internal memory store strategy, can be Replace or Merge - swapStrategy = ${?OTOROSHI_CLUSTER_WORKER_SWAP_STRATEGY} # the internal memory store strategy, can be Replace or Merge - modern = ${?CLUSTER_WORKER_STORE_MODERN} - modern = ${?OTOROSHI_CLUSTER_WORKER_STORE_MODERN} - useWs = ${?CLUSTER_WORKER_USE_WS} - useWs = ${?OTOROSHI_CLUSTER_WORKER_USE_WS} - state { - retries = ${otoroshi.cluster.worker.retries} # the number of retries when pulling state - retries = ${?CLUSTER_WORKER_STATE_RETRIES} # the number of retries when pulling state - retries = ${?OTOROSHI_CLUSTER_WORKER_STATE_RETRIES} # the number of retries when pulling state - pollEvery = ${?CLUSTER_WORKER_POLL_EVERY} # polling interval - pollEvery = ${?OTOROSHI_CLUSTER_WORKER_POLL_EVERY} # polling interval - timeout = ${otoroshi.cluster.worker.timeout} # the workers timeout when polling state - timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT} # the workers timeout when polling state - timeout = ${?OTOROSHI_CLUSTER_WORKER_POLL_TIMEOUT} # the workers timeout when polling state - } - quotas { - retries = ${otoroshi.cluster.worker.retries} # the number of retries when pushing quotas - retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES} # the number of retries when pushing quotas - retries = ${?OTOROSHI_CLUSTER_WORKER_QUOTAS_RETRIES} # the number of retries when pushing quotas - pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY} # pushing interval - pushEvery = ${?OTOROSHI_CLUSTER_WORKER_PUSH_EVERY} # pushing interval - timeout = ${otoroshi.cluster.worker.timeout} # the workers timeout when pushing quotas - timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT} # the workers timeout when pushing quotas - timeout = ${?OTOROSHI_CLUSTER_WORKER_PUSH_TIMEOUT} # the workers timeout when pushing quotas - } - } - analytics { # settings for the analytics actor system which is separated from otoroshi default one for performance reasons - pressure { - enabled = ${?OTOROSHI_ANALYTICS_PRESSURE_ENABLED} - } - actorsystem { - akka { - version = ${akka.version} - default-dispatcher { - fork-join-executor { - } - } - # http { - # parsing { - # max-chunk-size = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_CHUNK_SIZE} - # max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_CHUNK_SIZE} - # max-content-length = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_CONTENT_LENGHT} - # max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_CONTENT_LENGHT} - # max-to-strict-bytes = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_TO_STRICT_BYTES} - # max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_TO_STRICT_BYTES} - # } - # } - } - } - } - } - headers { # the default headers value for specific otoroshi headers - } - requests { - validate = ${?OTOROSHI_REQUESTS_VALIDATE} - maxUrlLength = ${akka.http.parsing.max-uri-length} - maxCookieLength = ${akka.http.parsing.max-header-value-length} - maxHeaderNameLength = ${akka.http.parsing.max-header-name-length} - maxHeaderValueLength = ${akka.http.parsing.max-header-value-length} - } - jmx { - enabled = ${?OTOROSHI_JMX_ENABLED} - port = ${?OTOROSHI_JMX_PORT} - } - loggers { - } - provider { - dashboardUrl = ${?OTOROSHI_PROVIDER_DASHBOARD_URL} - jsUrl = ${?OTOROSHI_PROVIDER_JS_URL} - cssUrl = ${?OTOROSHI_PROVIDER_CSS_URL} - secret = ${?OTOROSHI_PROVIDER_SECRET} - title = ${?OTOROSHI_PROVIDER_TITLE} - } - healthcheck { - workers = ${?OTOROSHI_HEALTHCHECK_WORKERS} - block-on-red = ${?OTOROSHI_HEALTHCHECK_BLOCK_ON_RED} - block-on-red = ${?OTOROSHI_HEALTHCHECK_BLOCK_ON_500} - ttl = ${?OTOROSHI_HEALTHCHECK_TTL} - ttl-only = ${?OTOROSHI_HEALTHCHECK_TTL_ONLY} - } - vaults { - enabled = ${?OTOROSHI_VAULTS_ENABLED} - secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL} - secrets-error-ttl = ${?OTOROSHI_VAULTS_SECRETS_ERROR_TTL} - cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS} - read-timeout = ${?otoroshi.vaults.read-ttl} - read-timeout = ${?OTOROSHI_VAULTS_READ_TTL} - read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT} - parallel-fetchs = ${?OTOROSHI_VAULTS_PARALLEL_FETCHS} - leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY} - env { - prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX} - } - local { - root = ${?OTOROSHI_VAULTS_LOCAL_ROOT} - } - # hashicorpvault { - # } - } - tunnels { - enabled = ${?OTOROSHI_TUNNELS_ENABLED} - worker-ws = ${?OTOROSHI_TUNNELS_WORKER_WS} - worker-use-internal-ports = ${?OTOROSHI_TUNNELS_WORKER_USE_INTERNAL_PORTS} - worker-use-loadbalancing = ${?OTOROSHI_TUNNELS_WORKER_USE_LOADBALANCING} - default { - enabled = ${?OTOROSHI_TUNNELS_DEFAULT_ENABLED} - id = ${?OTOROSHI_TUNNELS_DEFAULT_ID} - name = ${?OTOROSHI_TUNNELS_DEFAULT_NAME} - url = ${?OTOROSHI_TUNNELS_DEFAULT_URL} - host = ${?OTOROSHI_TUNNELS_DEFAULT_HOST} - clientId = ${?OTOROSHI_TUNNELS_DEFAULT_CLIENT_ID} - clientSecret = ${?OTOROSHI_TUNNELS_DEFAULT_CLIENT_SECRET} - export-routes = ${?OTOROSHI_TUNNELS_DEFAULT_EXPORT_ROUTES} # send routes information to remote otoroshi instance to facilitate remote route exposition - export-routes-tag = ${?OTOROSHI_TUNNELS_DEFAULT_EXPORT_TAG} # only send routes information if the route has this tag - proxy { - } - } - } - admin-extensions { - enabled = ${?OTOROSHI_ADMIN_EXTENSIONS_ENABLED} - configurations { - otoroshi_extensions_foo { - } - otoroshi_extensions_greenscore { - enabled = ${?OTOROSHI_ADMIN_EXTENSIONS_GREENSCORE_ENABLED} - } - } - } -} -http.port = ${?otoroshi.http.port} # the main http port for the otoroshi server -http.port = ${?PORT} # the main http port for the otoroshi server -http.port = ${?OTOROSHI_PORT} # the main http port for the otoroshi server -http.port = ${?OTOROSHI_HTTP_PORT} # the main http port for the otoroshi server -play.server.http.port = ${http.port} # the main http port for the otoroshi server -play.server.http.port = ${?PORT} # the main http port for the otoroshi server -play.server.http.port = ${?OTOROSHI_PORT} # the main http port for the otoroshi server -play.server.http.port = ${?OTOROSHI_HTTP_PORT} # the main http port for the otoroshi server -https.port = ${?otoroshi.https.port} # the main https port for the otoroshi server -https.port = ${?HTTPS_PORT} # the main https port for the otoroshi server -https.port = ${?OTOROSHI_HTTPS_PORT} # the main https port for the otoroshi server -play.server.https.keyStoreDumpPath = ${?HTTPS_KEYSTORE_DUMP_PATH} # the file path where the TLSContext will be dumped (for debugging purposes only) -play.server.https.keyStoreDumpPath = ${?OTOROSHI_HTTPS_KEYSTORE_DUMP_PATH} # the file path where the TLSContext will be dumped (for debugging purposes only) -play.http.secret.key = ${otoroshi.secret} # the secret used to signed session cookies -play.http.secret.key = ${?PLAY_CRYPTO_SECRET} # the secret used to signed session cookies -play.http.secret.key = ${?OTOROSHI_CRYPTO_SECRET} # the secret used to signed session cookies -play.server.http.idleTimeout = ${?PLAY_SERVER_IDLE_TIMEOUT} # the default server idle timeout -play.server.http.idleTimeout = ${?OTOROSHI_SERVER_IDLE_TIMEOUT} # the default server idle timeout -play.server.akka.requestTimeout = ${?PLAY_SERVER_REQUEST_TIMEOUT} # the default server idle timeout (for akka server specifically) -play.server.akka.requestTimeout = ${?OTOROSHI_SERVER_REQUEST_TIMEOUT} # the default server idle timeout (for akka server specifically) -http2.enabled = ${?otoroshi.http2.enabled} -http2.enabled = ${?HTTP2_ENABLED} # enable HTTP2 support -http2.enabled = ${?OTOROSHI_HTTP2_ENABLED} # enable HTTP2 support -play.server.https.keyStore.path=${?HTTPS_KEYSTORE_PATH} # settings for the default server keystore -play.server.https.keyStore.path=${?OTOROSHI_HTTPS_KEYSTORE_PATH} # settings for the default server keystore -play.server.https.keyStore.type=${?HTTPS_KEYSTORE_TYPE} # settings for the default server keystore -play.server.https.keyStore.type=${?OTOROSHI_HTTPS_KEYSTORE_TYPE} # settings for the default server keystore -play.server.https.keyStore.password=${?HTTPS_KEYSTORE_PASSWORD} # settings for the default server keystore -play.server.https.keyStore.password=${?OTOROSHI_HTTPS_KEYSTORE_PASSWORD} # settings for the default server keystore -play.server.https.keyStore.algorithm=${?HTTPS_KEYSTORE_ALGO} # settings for the default server keystore -play.server.https.keyStore.algorithm=${?OTOROSHI_HTTPS_KEYSTORE_ALGO} # settings for the default server keystore -play.server.websocket.frame.maxLength = ${?OTOROSHI_WEBSOCKET_FRAME_MAX_LENGTH} -play.http { - session { - secure = ${?SESSION_SECURE_ONLY} # the cookie for otoroshi backoffice should be exhanged over https only - secure = ${?OTOROSHI_SESSION_SECURE_ONLY} # the cookie for otoroshi backoffice should be exhanged over https only - maxAge = ${?SESSION_MAX_AGE} # the cookie for otoroshi backoffice max age - maxAge = ${?OTOROSHI_SESSION_MAX_AGE} # the cookie for otoroshi backoffice max age - # domain = "."${?app.domain} # the cookie for otoroshi backoffice domain - domain = "."${otoroshi.domain} # the cookie for otoroshi backoffice domain - domain = ${?SESSION_DOMAIN} # the cookie for otoroshi backoffice domain - domain = ${?OTOROSHI_SESSION_DOMAIN} # the cookie for otoroshi backoffice domain - cookieName = ${?SESSION_NAME} # the cookie for otoroshi backoffice name - cookieName = ${?OTOROSHI_SESSION_NAME} # the cookie for otoroshi backoffice name - } -} -akka { # akka specific configuration - actor { - default-dispatcher { - fork-join-executor { - parallelism-factor = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_FACTOR} - parallelism-min = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_MIN} - parallelism-max = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_MAX} - task-peeking-mode = ${?OTOROSHI_AKKA_DISPATCHER_TASK_PEEKING_MODE} - } - throughput = ${?OTOROSHI_AKKA_DISPATCHER_THROUGHPUT} - } - } - http { - server { - max-connections = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CONNECTIONS} - pipelining-limit = ${?OTOROSHI_AKKA_HTTP_SERVER_PIPELINING_LIMIT} - backlog = ${?OTOROSHI_AKKA_HTTP_SERVER_BACKLOG} - socket-options { - } - http2 { - } - } - client { - socket-options { - } - } - host-connection-pool { - max-connections = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_MAX_CONNECTIONS} - max-open-requests = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_MAX_OPEN_REQUESTS} - pipelining-limit = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_PIPELINING_LIMIT} - client { - socket-options { - } - } - } - parsing { - max-uri-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_URI_LENGTH} - max-method-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_METHOD_LENGTH} - max-response-reason-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_RESPONSE_REASON_LENGTH} - max-header-name-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_NAME_LENGTH} - max-header-value-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_VALUE_LENGTH} - max-header-count = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_COUNT} - max-chunk-ext-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CHUNK_EXT_LENGTH} - max-chunk-size = ${?AKKA_HTTP_SERVER_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CHUNK_SIZE} - max-content-length = ${?AKKA_HTTP_SERVER_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CONTENT_LENGHT} - } - } -} \ No newline at end of file diff --git a/manual/src/main/paradox/snippets/reference.conf b/manual/src/main/paradox/snippets/reference.conf index ff99cc171f..e69de29bb2 100644 --- a/manual/src/main/paradox/snippets/reference.conf +++ b/manual/src/main/paradox/snippets/reference.conf @@ -1,1712 +0,0 @@ - -app { - storage = "inmemory" # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql - storage = ${?APP_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql - storage = ${?OTOROSHI_STORAGE} # the storage used by otoroshi. possible values are lettuce (for redis), inmemory, file, http, s3, cassandra, postgresql - storageRoot = "otoroshi" # the prefix used for storage keys - storageRoot = ${?APP_STORAGE_ROOT} # the prefix used for storage keys - storageRoot = ${?OTOROSHI_STORAGE_ROOT} # the prefix used for storage keys - eventsName = "otoroshi" # the name of the event producer - eventsName = ${?APP_EVENTS_NAME} # the name of the event producer - eventsName = ${?OTOROSHI_EVENTS_NAME} # the name of the event producer - importFrom = ${?APP_IMPORT_FROM} # file path to import otoroshi initial configuration - importFrom = ${?OTOROSHI_IMPORT_FROM} # file path to import otoroshi initial configuration - env = "prod" # env name, should always be prod except in dev mode - env = ${?APP_ENV} # env name, should always be prod except in dev mode - env = ${?OTOROSHI_ENV} # env name, should always be prod except in dev mode - liveJs = false # enabled live JS loading for dev mode - domain = "oto.tools" # default domain for basic otoroshi services - domain = ${?APP_DOMAIN} # default domain for basic otoroshi services - domain = ${?OTOROSHI_DOMAIN} # default domain for basic otoroshi services - commitId = "HEAD" - commitId = ${?COMMIT_ID} - commitId = ${?OTOROSHI_COMMIT_ID} - rootScheme = "http" # default root scheme when composing urls - rootScheme = ${?APP_ROOT_SCHEME} # default root scheme when composing urls - rootScheme = ${?OTOROSHI_ROOT_SCHEME} # default root scheme when composing urls - throttlingWindow = 10 # the number of second used to compute throttling number - throttlingWindow = ${?THROTTLING_WINDOW} # the number of second used to compute throttling number - throttlingWindow = ${?OTOROSHI_THROTTLING_WINDOW} # the number of second used to compute throttling number - checkForUpdates = true # enable automatic version update checks - checkForUpdates = ${?CHECK_FOR_UPDATES} # enable automatic version update checks - checkForUpdates = ${?OTOROSHI_CHECK_FOR_UPDATES} # enable automatic version update checks - overheadThreshold = 500.0 # the value threshold (in milliseconds) used to send HighOverheadAlert - overheadThreshold = ${?OVERHEAD_THRESHOLD} # the value threshold (in milliseconds) used to send HighOverheadAlert - overheadThreshold = ${?OTOROSHI_OVERHEAD_THRESHOLD} # the value threshold (in milliseconds) used to send HighOverheadAlert - adminLogin = ${?OTOROSHI_INITIAL_ADMIN_LOGIN} # the initial admin login - adminPassword = ${?OTOROSHI_INITIAL_ADMIN_PASSWORD} # the initial admin password - initialCustomization = ${?OTOROSHI_INITIAL_CUSTOMIZATION} # otoroshi inital configuration that will be merged with a new confguration. Shaped like an otoroshi export - boot { - failOnTimeout = false # otoroshi will exit if a subsystem failed its init - failOnTimeout = ${?OTOROSHI_BOOT_FAIL_ON_TIMEOUT} # otoroshi will exit if a subsystem failed its init - - globalWait = true # should we wait until everything is setup to accept http requests - globalWait = ${?OTOROSHI_BOOT_GLOBAL_WAIT} # should we wait until everything is setup to accept http requests - globalWaitTimeout = 60000 # max wait before accepting requests - globalWaitTimeout = ${?OTOROSHI_BOOT_GLOBAL_WAIT_TIMEOUT} # max wait before accepting requests - - waitForPluginsSearch = true # should we wait for classpath plugins search before accepting http requests - waitForPluginsSearch = ${?OTOROSHI_BOOT_WAIT_FOR_PLUGINS_SEARCH} # should we wait for classpath plugins search before accepting http requests - waitForPluginsSearchTimeout = 20000 # max wait for classpath plugins search before accepting http requests - waitForPluginsSearchTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_PLUGINS_SEARCH_TIMEOUT} # max wait for classpath plugins search before accepting http requests - - waitForScriptsCompilation = true # should we wait for plugins compilation before accepting http requests - waitForScriptsCompilation = ${?OTOROSHI_BOOT_WAIT_FOR_SCRIPTS_COMPILATION} # should we wait for plugins compilation before accepting http requests - waitForScriptsCompilationTimeout = 30000 # max wait for plugins compilation before accepting http requests - waitForScriptsCompilationTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_SCRIPTS_COMPILATION_TIMEOUT} # max wait for plugins compilation before accepting http requests - - waitForTlsInit = true # should we wait for first TLS context initialization before accepting http requests - waitForTlsInit = ${?OTOROSHI_BOOT_WAIT_FOR_TLS_INIT} # should we wait for first TLS context initialization before accepting http requests - waitForTlsInitTimeout = 10000 # max wait for first TLS context initialization before accepting http requests - waitForTlsInitTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_TLS_INIT_TIMEOUT} # max wait for first TLS context initialization before accepting http requests - - waitForFirstClusterFetch = true # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterFetch = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_FETCH} # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterFetchTimeout = 10000 # max wait for first cluster initialization before accepting http requests - waitForFirstClusterFetchTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_TIMEOUT} # max wait for first cluster initialization before accepting http requests - - waitForFirstClusterStateCache = true # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterStateCache = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_STATE_CACHE} # should we wait for first cluster initialization before accepting http requests - waitForFirstClusterStateCacheTimeout = 10000 # max wait for first cluster initialization before accepting http requests - waitForFirstClusterStateCacheTimeout = ${?OTOROSHI_BOOT_WAIT_FOR_FIRST_CLUSTER_STATE_CACHE_TIMEOUT} # max wait for first cluster initialization before accepting http requests - } - instance { - instanceId = ${?OTOROSHI_INSTANCE_ID} # the instance id - number = 0 # the instance number. Can be found in otoroshi events - number = ${?OTOROSHI_INSTANCE_NUMBER} # the instance number. Can be found in otoroshi events - number = ${?INSTANCE_NUMBER} # the instance number. Can be found in otoroshi events - name = "otoroshi" # instance name - name = ${?OTOROSHI_INSTANCE_NAME} # instance name - logo = ${?OTOROSHI_INSTANCE_LOGO} # instance logo - zone = "local" # instance zone (optional) - zone = ${?OTOROSHI_INSTANCE_ZONE} # instance zone (optional) - region = "local" # instance region (optional) - region = ${?OTOROSHI_INSTANCE_REGION} # instance region (optional) - dc = "local" # instance dc (optional) - dc = ${?OTOROSHI_INSTANCE_DATACENTER} # instance dc (optional) - provider = "local" # instance provider (optional) - provider = ${?OTOROSHI_INSTANCE_PROVIDER} # instance provider (optional) - rack = "local" # instance rack (optional) - rack = ${?OTOROSHI_INSTANCE_RACK} # instance rack (optional) - title = ${?OTOROSHI_INSTANCE_TITLE} # the title displayed in UI top left - } - longRequestTimeout = 10800000 - longRequestTimeout = ${?OTOROSHI_PROXY_LONG_REQUEST_TIMEOUT} - } - health { - limit = 1000 # the value threshold (in milliseconds) used to indicate if an otoroshi instance is healthy or not - limit = ${?HEALTH_LIMIT} # the value threshold (in milliseconds) used to indicate if an otoroshi instance is healthy or not - limit = ${?OTOROSHI_HEALTH_LIMIT} # the value threshold (in milliseconds) used to indicate if an otoroshi instance is healthy or not - accessKey = ${?HEALTH_ACCESS_KEY} # the key to access /health edpoint - accessKey = ${?OTOROSHI_HEALTH_ACCESS_KEY} # the key to access /health edpoint - } - snowflake { - seed = 0 # the seed number used to generate unique ids. Should be different for every instances - seed = ${?INSTANCE_NUMBER} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?OTOROSHI_INSTANCE_NUMBER} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?SNOWFLAKE_SEED} # the seed number used to generate unique ids. Should be different for every instances - seed = ${?OTOROSHI_SNOWFLAKE_SEED} # the seed number used to generate unique ids. Should be different for every instances - } - events { - maxSize = 1000 # the amount of event kept in the datastore - maxSize = ${?MAX_EVENTS_SIZE} # the amount of event kept in the datastore - maxSize = ${?OTOROSHI_MAX_EVENTS_SIZE} # the amount of event kept in the datastore - } - exposed-ports { - http = ${?APP_EXPOSED_PORTS_HTTP} # the exposed http port for otoroshi (when in a container or behind a proxy) - http = ${?OTOROSHI_EXPOSED_PORTS_HTTP} # the exposed http port for otoroshi (when in a container or behind a proxy) - https = ${?APP_EXPOSED_PORTS_HTTPS} # the exposed https port for otoroshi (when in a container or behind a proxy - https = ${?OTOROSHI_EXPOSED_PORTS_HTTPS} # the exposed https port for otoroshi (when in a container or behind a proxy - } - backoffice { - exposed = true # expose the backoffice ui - exposed = ${?APP_BACKOFFICE_EXPOSED} # expose the backoffice ui - exposed = ${?OTOROSHI_BACKOFFICE_EXPOSED} # expose the backoffice ui - subdomain = "otoroshi" # the backoffice subdomain - subdomain = ${?APP_BACKOFFICE_SUBDOMAIN} # the backoffice subdomain - subdomain = ${?OTOROSHI_BACKOFFICE_SUBDOMAIN} # the backoffice subdomain - domains = [] # the backoffice domains - domainsStr = ${?APP_BACKOFFICE_DOMAINS} # the backoffice domains - domainsStr = ${?OTOROSHI_BACKOFFICE_DOMAINS} # the backoffice domains - # useNewEngine = false # avoid backoffice admin api proxy - # useNewEngine = ${?OTOROSHI_BACKOFFICE_USE_NEW_ENGINE} # avoid backoffice admin api proxy - usePlay = true # avoid backoffice http call for admin api - usePlay = ${?OTOROSHI_BACKOFFICE_USE_PLAY} # avoid backoffice http call for admin api - session { - exp = 86400000 # the backoffice cookie expiration - exp = ${?APP_BACKOFFICE_SESSION_EXP} # the backoffice cookie expiration - exp = ${?OTOROSHI_BACKOFFICE_SESSION_EXP} # the backoffice cookie expiration - } - } - privateapps { - subdomain = "privateapps" # privateapps (proxy sso) domain - subdomain = ${?APP_PRIVATEAPPS_SUBDOMAIN} # privateapps (proxy sso) domain - subdomain = ${?OTOROSHI_PRIVATEAPPS_SUBDOMAIN} # privateapps (proxy sso) domain - domains = [] - domainsStr = ${?APP_PRIVATEAPPS_DOMAINS} - domainsStr = ${?OTOROSHI_PRIVATEAPPS_DOMAINS} - session { - exp = 86400000 # the privateapps cookie expiration - exp = ${?APP_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration - exp = ${?OTOROSHI_PRIVATEAPPS_SESSION_EXP} # the privateapps cookie expiration - } - } - adminapi { - exposed = true # expose the admin api - exposed = ${?ADMIN_API_EXPOSED} # expose the admin api - exposed = ${?OTOROSHI_ADMIN_API_EXPOSED} # expose the admin api - targetSubdomain = "otoroshi-admin-internal-api" # admin api target subdomain as targeted by otoroshi service - targetSubdomain = ${?ADMIN_API_TARGET_SUBDOMAIN} # admin api target subdomain as targeted by otoroshi service - targetSubdomain = ${?OTOROSHI_ADMIN_API_TARGET_SUBDOMAIN} # admin api target subdomain as targeted by otoroshi service - exposedSubdomain = "otoroshi-api" # admin api exposed subdomain as exposed by otoroshi service - exposedSubdomain = ${?ADMIN_API_EXPOSED_SUBDOMAIN} # admin api exposed subdomain as exposed by otoroshi service - exposedSubdomain = ${?OTOROSHI_ADMIN_API_EXPOSED_SUBDOMAIN} # admin api exposed subdomain as exposed by otoroshi service - additionalExposedDomain = ${?ADMIN_API_ADDITIONAL_EXPOSED_DOMAIN} # admin api additional exposed subdomain as exposed by otoroshi service - additionalExposedDomain = ${?OTOROSHI_ADMIN_API_ADDITIONAL_EXPOSED_DOMAIN} # admin api additional exposed subdomain as exposed by otoroshi service - domains = [] - domainsStr = ${?ADMIN_API_DOMAINS} - domainsStr = ${?OTOROSHI_ADMIN_API_DOMAINS} - exposedDomains = [] - exposedDomainsStr = ${?ADMIN_API_EXPOSED_DOMAINS} - exposedDomainsStr = ${?OTOROSHI_ADMIN_API_EXPOSED_DOMAINS} - defaultValues { - backOfficeGroupId = "admin-api-group" # default value for admin api service group - backOfficeGroupId = ${?ADMIN_API_GROUP} # default value for admin api service group - backOfficeGroupId = ${?OTOROSHI_ADMIN_API_GROUP} # default value for admin api service group - backOfficeApiKeyClientId = "admin-api-apikey-id" # default value for admin api apikey id - backOfficeApiKeyClientId = ${?ADMIN_API_CLIENT_ID} # default value for admin api apikey id - backOfficeApiKeyClientId = ${?OTOROSHI_ADMIN_API_CLIENT_ID} # default value for admin api apikey id - backOfficeApiKeyClientSecret = "admin-api-apikey-secret" # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?otoroshi.admin-api-secret} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?OTOROSHI_otoroshi.admin-api-secret} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?ADMIN_API_CLIENT_SECRET} # default value for admin api apikey secret - backOfficeApiKeyClientSecret = ${?OTOROSHI_ADMIN_API_CLIENT_SECRET} # default value for admin api apikey secret - backOfficeServiceId = "admin-api-service" # default value for admin api service id - backOfficeServiceId = ${?ADMIN_API_SERVICE_ID} # default value for admin api service id - backOfficeServiceId = ${?OTOROSHI_ADMIN_API_SERVICE_ID} # default value for admin api service id - } - proxy { - https = false # backoffice proxy admin api over https - https = ${?ADMIN_API_HTTPS} # backoffice proxy admin api over https - https = ${?OTOROSHI_ADMIN_API_HTTPS} # backoffice proxy admin api over https - local = true # backoffice proxy admin api on localhost - local = ${?ADMIN_API_LOCAL} # backoffice proxy admin api on localhost - local = ${?OTOROSHI_ADMIN_API_LOCAL} # backoffice proxy admin api on localhost - } - } - claim { - sharedKey = "secret" # the default secret used to sign otoroshi exchange protocol tokens - sharedKey = ${?CLAIM_SHAREDKEY} # the default secret used to sign otoroshi exchange protocol tokens - sharedKey = ${?OTOROSHI_CLAIM_SHAREDKEY} # the default secret used to sign otoroshi exchange protocol tokens - } - webhooks { - } - redis { # configuration to fetch/store otoroshi state from a redis datastore using rediscala - host = "localhost" - host = ${?REDIS_HOST} - host = ${?OTOROSHI_REDIS_HOST} - port = 6379 - port = ${?REDIS_PORT} - port = ${?OTOROSHI_REDIS_PORT} - password = ${?REDIS_PASSWORD} - password = ${?OTOROSHI_REDIS_PASSWORD} - windowSize = 99 - windowSize = ${?REDIS_WINDOW_SIZE} - windowSize = ${?OTOROSHI_REDIS_WINDOW_SIZE} - slaves = [] - slavesStr = ${?REDIS_SLAVES} - slavesStr = ${?OTOROSHI_REDIS_SLAVES} - slavesStr = ${?REDIS_MEMBERS} - slavesStr = ${?OTOROSHI_REDIS_MEMBERS} - useScan = false - useScan = ${?REDIS_USE_SCAN} - useScan = ${?OTOROSHI_REDIS_USE_SCAN} - - pool { - members = [] - members = ${?REDIS_POOL_MEMBERS} - members = ${?OTOROSHI_REDIS_POOL_MEMBERS} - } - - mpool { - members = [] - membersStr = ${?REDIS_MPOOL_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_MPOOL_MEMBERS} - } - - lf { - master { - host = ${?REDIS_LF_HOST} - host = ${?OTOROSHI_REDIS_LF_HOST} - port = ${?REDIS_LF_PORT} - port = ${?OTOROSHI_REDIS_LF_PORT} - password = ${?REDIS_LF_PASSWORD} - password = ${?OTOROSHI_REDIS_LF_PASSWORD} - } - slaves = [] - slavesStr = ${?REDIS_LF_SLAVES} - slavesStr = ${?OTOROSHI_REDIS_LF_SLAVES} - slavesStr = ${?REDIS_LF_MEMBERS} - slavesStr = ${?OTOROSHI_REDIS_LF_MEMBERS} - } - - sentinels { - master = ${?REDIS_SENTINELS_MASTER} - master = ${?OTOROSHI_REDIS_SENTINELS_MASTER} - password = ${?REDIS_SENTINELS_PASSWORD} - password = ${?OTOROSHI_REDIS_SENTINELS_PASSWORD} - db = ${?REDIS_SENTINELS_DB} - db = ${?OTOROSHI_REDIS_SENTINELS_DB} - name = ${?REDIS_SENTINELS_NAME} - name = ${?OTOROSHI_REDIS_SENTINELS_NAME} - members = [] - membersStr = ${?REDIS_SENTINELS_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_SENTINELS_MEMBERS} - - lf { - master = ${?REDIS_SENTINELS_LF_MASTER} - master = ${?OTOROSHI_REDIS_SENTINELS_LF_MASTER} - members = [] - membersStr = ${?REDIS_SENTINELS_LF_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_SENTINELS_LF_MEMBERS} - } - } - - cluster { - members = [] - membersStr = ${?REDIS_CLUSTER_MEMBERS} - membersStr = ${?OTOROSHI_REDIS_CLUSTER_MEMBERS} - } - - lettuce { # configuration to fetch/store otoroshi state from a redis datastore using the lettuce driver (the next default one) - connection = "default" - connection = ${?REDIS_LETTUCE_CONNECTION} - connection = ${?OTOROSHI_REDIS_LETTUCE_CONNECTION} - uri = ${?REDIS_LETTUCE_URI} - uri = ${?OTOROSHI_REDIS_LETTUCE_URI} - uri = ${?REDIS_URL} - uri = ${?OTOROSHI_REDIS_URL} - uris = [] - urisStr = ${?REDIS_LETTUCE_URIS} - urisStr = ${?OTOROSHI_REDIS_LETTUCE_URIS} - readFrom = "MASTER_PREFERRED" - readFrom = ${?REDIS_LETTUCE_READ_FROM} - readFrom = ${?OTOROSHI_REDIS_LETTUCE_READ_FROM} - startTLS = false - startTLS = ${?REDIS_LETTUCE_START_TLS} - startTLS = ${?OTOROSHI_REDIS_LETTUCE_START_TLS} - verifyPeers = true - verifyPeers = ${?REDIS_LETTUCE_VERIFY_PEERS} - verifyPeers = ${?OTOROSHI_REDIS_LETTUCE_VERIFY_PEERS} - } - } - inmemory { # configuration to fetch/store otoroshi state in memory - windowSize = 99 - windowSize = ${?INMEMORY_WINDOW_SIZE} - windowSize = ${?OTOROSHI_INMEMORY_WINDOW_SIZE} - experimental = false - experimental = ${?INMEMORY_EXPERIMENTAL_STORE} - experimental = ${?OTOROSHI_INMEMORY_EXPERIMENTAL_STORE} - optimized = false - optimized = ${?INMEMORY_OPTIMIZED} - optimized = ${?OTOROSHI_INMEMORY_OPTIMIZED} - modern = false - modern = ${?INMEMORY_MODERN} - modern = ${?OTOROSHI_INMEMORY_MODERN} - } - filedb { # configuration to fetch/store otoroshi state from a file - windowSize = 99 - windowSize = ${?FILEDB_WINDOW_SIZE} - windowSize = ${?OTOROSHI_FILEDB_WINDOW_SIZE} - path = "./filedb/state.ndjson" - path = ${?FILEDB_PATH} - path = ${?OTOROSHI_FILEDB_PATH} - } - httpdb { # configuration to fetch/store otoroshi state from an http endpoint - url = "http://127.0.0.1:8888/worker-0/state.json" - headers = {} - timeout = 10000 - pollEvery = 10000 - } - s3db { # configuration to fetch/store otoroshi state from a S3 bucket - bucket = "otoroshi-states" - bucket = ${?OTOROSHI_DB_S3_BUCKET} - endpoint = "https://otoroshi-states.foo.bar" - endpoint = ${?OTOROSHI_DB_S3_ENDPOINT} - region = "eu-west-1" - region = ${?OTOROSHI_DB_S3_REGION} - access = "secret" - access = ${?OTOROSHI_DB_S3_ACCESS} - secret = "secret" - secret = ${?OTOROSHI_DB_S3_SECRET} - key = "/otoroshi/states/state" - key = ${?OTOROSHI_DB_S3_KEY} - chunkSize = 8388608 - chunkSize = ${?OTOROSHI_DB_S3_CHUNK_SIZE} - v4auth = true - v4auth = ${?OTOROSHI_DB_S3_V4_AUTH} - writeEvery = 60000 # write interval - writeEvery = ${?OTOROSHI_DB_S3_WRITE_EVERY} # write interval - acl = "Private" - acl = ${?OTOROSHI_DB_S3_ACL} - } - pg { # postrgesql settings. everything possible with the client - uri = ${?PG_URI} - uri = ${?OTOROSHI_PG_URI} - uri = ${?POSTGRESQL_ADDON_URI} - uri = ${?OTOROSHI_POSTGRESQL_ADDON_URI} - poolSize = 20 - poolSize = ${?PG_POOL_SIZE} - poolSize = ${?OTOROSHI_PG_POOL_SIZE} - port = 5432 - port = ${?PG_PORT} - port = ${?OTOROSHI_PG_PORT} - host = "localhost" - host = ${?PG_HOST} - host = ${?OTOROSHI_PG_HOST} - database = "otoroshi" - database = ${?PG_DATABASE} - database = ${?OTOROSHI_PG_DATABASE} - user = "otoroshi" - user = ${?PG_USER} - user = ${?OTOROSHI_PG_USER} - password = "otoroshi" - password = ${?PG_PASSWORD} - password = ${?OTOROSHI_PG_PASSWORD} - logQueries = ${?PG_DEBUG_QUERIES} - logQueries = ${?OTOROSHI_PG_DEBUG_QUERIES} - avoidJsonPath = false - avoidJsonPath = ${?PG_AVOID_JSON_PATH} - avoidJsonPath = ${?OTOROSHI_PG_AVOID_JSON_PATH} - optimized = true - optimized = ${?PG_OPTIMIZED} - optimized = ${?OTOROSHI_PG_OPTIMIZED} - connect-timeout = ${?PG_CONNECT_TIMEOUT} - connect-timeout = ${?OTOROSHI_PG_CONNECT_TIMEOUT} - idle-timeout = ${?PG_IDLE_TIMEOUT} - idle-timeout = ${?OTOROSHI_PG_IDLE_TIMEOUT} - log-activity = ${?PG_LOG_ACTIVITY} - log-activity = ${?OTOROSHI_PG_LOG_ACTIVITY} - pipelining-limit = ${?PG_PIPELINING_LIMIT} - pipelining-limit = ${?OTOROSHI_PG_PIPELINING_LIMIT} - ssl { - enabled = false - enabled = ${?PG_SSL_ENABLED} - enabled = ${?OTOROSHI_PG_SSL_ENABLED} - mode = "verify_ca" - mode = ${?PG_SSL_MODE} - mode = ${?OTOROSHI_PG_SSL_MODE} - trusted-certs-path = [] - trusted-certs = [] - trusted-cert-path = ${?PG_SSL_TRUSTED_CERT_PATH} - trusted-cert-path = ${?OTOROSHI_PG_SSL_TRUSTED_CERT_PATH} - trusted-cert = ${?PG_SSL_TRUSTED_CERT} - trusted-cert = ${?OTOROSHI_PG_SSL_TRUSTED_CERT} - client-certs-path = [] - client-certs = [] - client-cert-path = ${?PG_SSL_CLIENT_CERT_PATH} - client-cert-path = ${?OTOROSHI_PG_SSL_CLIENT_CERT_PATH} - client-cert = ${?PG_SSL_CLIENT_CERT} - client-cert = ${?OTOROSHI_PG_SSL_CLIENT_CERT} - trust-all = ${?PG_SSL_TRUST_ALL} - trust-all = ${?OTOROSHI_PG_SSL_TRUST_ALL} - } - } - cassandra { # cassandra settings. everything possible with the client - windowSize = 99 - windowSize = ${?CASSANDRA_WINDOW_SIZE} - windowSize = ${?OTOROSHI_CASSANDRA_WINDOW_SIZE} - host = "127.0.0.1" - host = ${?CASSANDRA_HOST} - host = ${?OTOROSHI_CASSANDRA_HOST} - port = 9042 - port = ${?CASSANDRA_PORT} - port = ${?OTOROSHI_CASSANDRA_PORT} - replicationFactor = 1 - replicationFactor = ${?CASSANDRA_REPLICATION_FACTOR} - replicationFactor = ${?OTOROSHI_CASSANDRA_REPLICATION_FACTOR} - replicationOptions = ${?CASSANDRA_REPLICATION_OPTIONS} - replicationOptions = ${?OTOROSHI_CASSANDRA_REPLICATION_OPTIONS} - durableWrites = true - durableWrites = ${?CASSANDRA_DURABLE_WRITES} - durableWrites = ${?OTOROSHI_CASSANDRA_DURABLE_WRITES} - basic.contact-points = [ ${app.cassandra.host}":"${app.cassandra.port} ] - basic.session-name = "otoroshi" - basic.session-name = ${?OTOROSHI_CASSANDRA_SESSION_NAME} - basic.session-keyspace = ${?OTOROSHI_CASSANDRA_SESSION_KEYSPACE} - basic.config-reload-interval = 5 minutes - basic.request { - timeout = 10 seconds - consistency = LOCAL_ONE - consistency = ${?OTOROSHI_CASSANDRA_CONSISTENCY} - page-size = 5000 - page-size = ${?OTOROSHI_CASSANDRA_PAGE_SIZE} - serial-consistency = SERIAL - serial-consistency = ${?OTOROSHI_CASSANDRA_SERIAL_CONSISTENCY} - default-idempotence = false - default-idempotence = ${?OTOROSHI_CASSANDRA_DEFAULT_IDEMPOTENCE} - } - basic.load-balancing-policy { - class = DefaultLoadBalancingPolicy - local-datacenter = datacenter1 - local-datacenter = ${?OTOROSHI_CASSANDRA_LOCAL_DATACENTER} - # filter.class= - slow-replica-avoidance = true - } - basic.cloud { - # secure-connect-bundle = /location/of/secure/connect/bundle - } - basic.application { - # name = - # version = - } - basic.graph { - # name = your-graph-name - traversal-source = "g" - # is-system-query = false - # read-consistency-level = LOCAL_QUORUM - # write-consistency-level = LOCAL_ONE - # timeout = 10 seconds - } - advanced.connection { - connect-timeout = 5 seconds - init-query-timeout = 500 milliseconds - set-keyspace-timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - pool { - local { - size = 1 - } - remote { - size = 1 - } - } - max-requests-per-connection = 1024 - max-orphan-requests = 256 - warn-on-init-error = true - } - advanced.reconnect-on-init = false - advanced.reconnection-policy { - class = ExponentialReconnectionPolicy - base-delay = 1 second - max-delay = 60 seconds - } - advanced.retry-policy { - class = DefaultRetryPolicy - } - advanced.speculative-execution-policy { - class = NoSpeculativeExecutionPolicy - # max-executions = 3 - # delay = 100 milliseconds - } - advanced.auth-provider { - # class = PlainTextAuthProvider - username = ${?CASSANDRA_USERNAME} - username = ${?OTOROSHI_CASSANDRA_USERNAME} - password = ${?CASSANDRA_PASSWORD} - password = ${?OTOROSHI_CASSANDRA_PASSWORD} - authorization-id = ${?OTOROSHI_CASSANDRA_AUTHORIZATION_ID} - //service = "cassandra" - # login-configuration { - # principal = "cassandra@DATASTAX.COM" - # useKeyTab = "true" - # refreshKrb5Config = "true" - # keyTab = "/path/to/keytab/file" - # } - # sasl-properties { - # javax.security.sasl.qop = "auth-conf" - # } - } - advanced.ssl-engine-factory { - # class = DefaultSslEngineFactory - # cipher-suites = [ "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA" ] - # hostname-validation = true - # truststore-path = /path/to/client.truststore - # truststore-password = password123 - # keystore-path = /path/to/client.keystore - # keystore-password = password123 - } - advanced.timestamp-generator { - class = AtomicTimestampGenerator - drift-warning { - threshold = 1 second - interval = 10 seconds - } - force-java-clock = false - } - advanced.request-tracker { - class = NoopRequestTracker - logs { - # success.enabled = true - slow { - # threshold = 1 second - # enabled = true - } - # error.enabled = true - # max-query-length = 500 - # show-values = true - # max-value-length = 50 - # max-values = 50 - # show-stack-traces = true - } - } - advanced.throttler { - class = PassThroughRequestThrottler - # max-queue-size = 10000 - # max-concurrent-requests = 10000 - # max-requests-per-second = 10000 - # drain-interval = 10 milliseconds - } - advanced.node-state-listener.class = NoopNodeStateListener - advanced.schema-change-listener.class = NoopSchemaChangeListener - advanced.address-translator { - class = PassThroughAddressTranslator - } - advanced.resolve-contact-points = true - advanced.protocol { - version = V4 - version = ${?OTOROSHI_CASSANDRA_PROTOCOL_VERSION} - compression = lz4 - compression = ${?OTOROSHI_CASSANDRA_PROTOCOL_COMPRESSION} - max-frame-length = 256 MB - } - advanced.request { - warn-if-set-keyspace = false - trace { - attempts = 5 - interval = 3 milliseconds - consistency = ONE - } - log-warnings = true - } - advanced.graph { - # sub-protocol = "graphson-2.0" - paging-enabled = "AUTO" - paging-options { - page-size = ${datastax-java-driver.advanced.continuous-paging.page-size} - max-pages = ${datastax-java-driver.advanced.continuous-paging.max-pages} - max-pages-per-second = ${datastax-java-driver.advanced.continuous-paging.max-pages-per-second} - max-enqueued-pages = ${datastax-java-driver.advanced.continuous-paging.max-enqueued-pages} - } - } - advanced.continuous-paging { - page-size = ${datastax-java-driver.basic.request.page-size} - page-size-in-bytes = false - max-pages = 0 - max-pages-per-second = 0 - max-enqueued-pages = 4 - timeout { - first-page = 2 seconds - other-pages = 1 second - } - } - advanced.monitor-reporting { - enabled = true - } - advanced.metrics { - session { - enabled = [ - # bytes-sent, - # bytes-received - # connected-nodes, - # cql-requests, - # cql-client-timeouts, - # cql-prepared-cache-size, - # throttling.delay, - # throttling.queue-size, - # throttling.errors, - # continuous-cql-requests, - # graph-requests, - # graph-client-timeouts - ] - cql-requests { - highest-latency = 3 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - throttling.delay { - highest-latency = 3 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - continuous-cql-requests { - highest-latency = 120 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - graph-requests { - highest-latency = 12 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - } - node { - enabled = [ - # pool.open-connections, - # pool.available-streams, - # pool.in-flight, - # pool.orphaned-streams, - # bytes-sent, - # bytes-received, - # cql-messages, - # errors.request.unsent, - # errors.request.aborted, - # errors.request.write-timeouts, - # errors.request.read-timeouts, - # errors.request.unavailables, - # errors.request.others, - # retries.total, - # retries.aborted, - # retries.read-timeout, - # retries.write-timeout, - # retries.unavailable, - # retries.other, - # ignores.total, - # ignores.aborted, - # ignores.read-timeout, - # ignores.write-timeout, - # ignores.unavailable, - # ignores.other, - # speculative-executions, - # errors.connection.init, - # errors.connection.auth, - # graph-messages, - ] - cql-messages { - highest-latency = 3 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - graph-messages { - highest-latency = 3 seconds - significant-digits = 3 - refresh-interval = 5 minutes - } - } - } - advanced.socket { - tcp-no-delay = true - //keep-alive = false - //reuse-address = true - //linger-interval = 0 - //receive-buffer-size = 65535 - //send-buffer-size = 65535 - } - advanced.heartbeat { - interval = 30 seconds - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - } - advanced.metadata { - topology-event-debouncer { - window = 1 second - max-events = 20 - } - schema { - enabled = true - # refreshed-keyspaces = [ "ks1", "ks2" ] - request-timeout = ${datastax-java-driver.basic.request.timeout} - request-page-size = ${datastax-java-driver.basic.request.page-size} - debouncer { - window = 1 second - max-events = 20 - } - } - token-map.enabled = true - } - advanced.control-connection { - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - schema-agreement { - interval = 200 milliseconds - timeout = 10 seconds - warn-on-failure = true - } - } - advanced.prepared-statements { - prepare-on-all-nodes = true - reprepare-on-up { - enabled = true - check-system-table = false - max-statements = 0 - max-parallelism = 100 - timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} - } - } - advanced.netty { - daemon = false - io-group { - size = 0 - shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} - } - admin-group { - size = 2 - shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} - } - timer { - tick-duration = 100 milliseconds - ticks-per-wheel = 2048 - } - } - advanced.coalescer { - max-runs-with-no-work = 5 - reschedule-interval = 10 microseconds - } - } - actorsystems { - otoroshi { - akka { # otoroshi actorsystem configuration - version = ${akka.version} - log-dead-letters-during-shutdown = false - jvm-exit-on-fatal-error = false - default-dispatcher { - type = Dispatcher - executor = "fork-join-executor" - fork-join-executor { - parallelism-factor = 4.0 - parallelism-factor = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_FACTOR} - parallelism-min = 8 - parallelism-min = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_MIN} - parallelism-max = 128 - parallelism-max = ${?OTOROSHI_CORE_DISPATCHER_PARALLELISM_MAX} - task-peeking-mode = "FIFO" - task-peeking-mode = ${?OTOROSHI_CORE_DISPATCHER_TASK_PEEKING_MODE} - } - throughput = 1 - throughput = ${?OTOROSHI_CORE_DISPATCHER_THROUGHPUT} - } - http { - parsing { - max-uri-length = 4k - max-uri-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_URI_LENGTH} - max-method-length = 16 - max-method-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_METHOD_LENGTH} - max-response-reason-length = 64 - max-response-reason-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_RESPONSE_REASON_LENGTH} - max-header-name-length = 128 - max-header-name-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_NAME_LENGTH} - max-header-value-length = 16k - max-header-value-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_VALUE_LENGTH} - max-header-count = 128 - max-header-count = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_HEADER_COUNT} - max-chunk-ext-length = 256 - max-chunk-ext-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CHUNK_EXT_LENGTH} - max-chunk-size = 256m - max-chunk-size = ${?AKKA_HTTP_CLIENT_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CHUNK_SIZE} - max-content-length = infinite - max-content-length = ${?AKKA_HTTP_CLIENT_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_CONTENT_LENGHT} - max-to-strict-bytes = infinite - max-to-strict-bytes = ${?AKKA_HTTP_CLIENT_MAX_TO_STRICT_BYTES} - max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_MAX_TO_STRICT_BYTES} - max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_PARSING_MAX_TO_STRICT_BYTES} - } - } - } - } - datastore { - akka { - version = ${akka.version} - log-dead-letters-during-shutdown = false - jvm-exit-on-fatal-error = false - default-dispatcher { - type = Dispatcher - executor = "fork-join-executor" - fork-join-executor { - parallelism-factor = 4.0 - parallelism-min = 4 - parallelism-max = 64 - task-peeking-mode = "FIFO" - } - throughput = 1 - } - } - } - } -} - -otoroshi { - domain = ${?app.domain} - maintenanceMode = false # enable global maintenance mode - maintenanceMode = ${?OTOROSHI_MAINTENANCE_MODE_ENABLED} # enable global maintenance mode - secret = "verysecretvaluethatyoumustoverwrite" # the secret used to sign sessions - secret = ${?OTOROSHI_SECRET} # the secret used to sign sessions - admin-api-secret = ${?OTOROSHI_ADMIN_API_SECRET} # the secret for admin api - elSettings { - allowEnvAccess = true - allowEnvAccess = ${?OTOROSHI_EL_SETTINGS_ALLOW_ENV_ACCESS} - allowConfigAccess = true - allowConfigAccess = ${?OTOROSHI_EL_SETTINGS_ALLOW_CONFIG_ACCESS} - } - open-telemetry { - server-logs { - enabled = false - enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENABLED} - gzip = false - gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GZIP} - grpc = false - grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_GRPC} - endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_ENDPOINT} - timeout = 5000 - timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TIMEOUT} - client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_CLIENT_CERT} - trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_TRUSTED_CERT} - headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_HEADERS} - max_batch = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_BATCH} - max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_LOGS_MAX_DURATION} - } - server-metrics { - enabled = false - enabled = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENABLED} - gzip = false - gzip = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GZIP} - grpc = false - grpc = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_GRPC} - endpoint = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_ENDPOINT} - timeout = 5000 - timeout = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TIMEOUT} - client_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_CLIENT_CERT} - trusted_cert = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_TRUSTED_CERT} - headers = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_HEADERS} - max_batch = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_BATCH} - max_duration = ${?OTOROSHI_OPEN_TELEMETRY_SERVER_METRICS_MAX_DURATION} - } - } - next { - state-sync-interval = 10000 - state-sync-interval = ${?OTOROSHI_NEXT_STATE_SYNC_INTERVAL} - export-reporting = false - export-reporting = ${?OTOROSHI_NEXT_EXPORT_REPORTING} - monitor-proxy-state-size = false - monitor-proxy-state-size = ${?OTOROSHI_NEXT_MONITOR_PROXY_STATE_SIZE} - monitor-datastore-size = false - monitor-datastore-size = ${?OTOROSHI_NEXT_MONITOR_DATASTORE_SIZE} - plugins { - merge-sync-steps = true - merge-sync-steps = ${?OTOROSHI_NEXT_PLUGINS_MERGE_SYNC_STEPS} - apply-legacy-checks = true - apply-legacy-checks = ${?OTOROSHI_NEXT_PLUGINS_APPLY_LEGACY_CHECKS} - } - experimental { - netty-client { - wiretap = false - wiretap = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_WIRETAP} - enforce = false - enforce = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_ENFORCE} - enforce-akka = false - enforce-akka = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_CLIENT_ENFORCE_AKKA} - } - netty-server { - enabled = false - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ENABLED} - new-engine-only = false - new-engine-only = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NEW_ENGINE_ONLY} - host = "0.0.0.0" - host = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HOST} - http-port = 10049 - http-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_PORT} - exposed-http-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_EXPOSED_HTTP_PORT} - https-port = 10048 - https-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTPS_PORT} - exposed-https-port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_EXPOSED_HTTPS_PORT} - wiretap = false - wiretap = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_WIRETAP} - accesslog = false - accesslog = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_ACCESSLOG} - threads = 0 - threads = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_THREADS} - parser { - allowDuplicateContentLengths = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_ALLOW_DUPLICATE_CONTENT_LENGTHS} - validateHeaders = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_VALIDATE_HEADERS} - h2cMaxContentLength = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_H_2_C_MAX_CONTENT_LENGTH} - initialBufferSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_INITIAL_BUFFER_SIZE} - maxHeaderSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_HEADER_SIZE} - maxInitialLineLength = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_INITIAL_LINE_LENGTH} - maxChunkSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_PARSER_MAX_CHUNK_SIZE} - } - http2 { - enabled = true - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_ENABLED} - h2c = true - h2c = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP2_H2C} - } - http3 { - enabled = false - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_ENABLED} - port = 10048 - port = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_PORT} - exposedPort = 10048 - exposedPort = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP3_EXPOSED_PORT} - initialMaxStreamsBidirectional = 100000 - initialMaxStreamsBidirectional = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAMS_BIDIRECTIONAL} - initialMaxStreamDataBidirectionalRemote = 1000000 - initialMaxStreamDataBidirectionalRemote = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE} - initialMaxStreamDataBidirectionalLocal = 1000000 - initialMaxStreamDataBidirectionalLocal = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL} - initialMaxData = 10000000 - initialMaxData = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_INITIAL_MAX_DATA} - maxRecvUdpPayloadSize = 1500 - maxRecvUdpPayloadSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_RECV_UDP_PAYLOAD_SIZE} - maxSendUdpPayloadSize = 1500 - maxSendUdpPayloadSize = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_MAX_SEND_UDP_PAYLOAD_SIZE} - disableQpackDynamicTable = true - disableQpackDynamicTable = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_HTTP_3_DISABLE_QPACK_DYNAMIC_TABLE} - } - native { - enabled = true - enabled = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_ENABLED} - driver = "Auto" # possible values are Auto, Epoll, KQueue, IOUring - driver = ${?OTOROSHI_NEXT_EXPERIMENTAL_NETTY_SERVER_NATIVE_DRIVER} - } - } - } - } - options { - bypassUserRightsCheck = false - bypassUserRightsCheck = ${?OTOROSHI_OPTIONS_BYPASSUSERRIGHTSCHECK} - emptyContentLengthIsChunked = true - emptyContentLengthIsChunked = ${?OTOROSHI_OPTIONS_EMPTYCONTENTLENGTHISCHUNKED} - detectApiKeySooner = true - detectApiKeySooner = ${?OTOROSHI_OPTIONS_DETECTAPIKEYSOONER} - sendClientChainAsPem = false - sendClientChainAsPem = ${?OTOROSHI_OPTIONS_SENDCLIENTCHAINASPEM} - useOldHeadersComposition = false - useOldHeadersComposition = ${?OTOROSHI_OPTIONS_USEOLDHEADERSCOMPOSITION} - manualDnsResolve = true - manualDnsResolve = ${?OTOROSHI_OPTIONS_MANUALDNSRESOLVE} - useEventStreamForScriptEvents = true - useEventStreamForScriptEvents = ${?OTOROSHI_OPTIONS_USEEVENTSTREAMFORSCRIPTEVENTS} - trustXForwarded = true - trustXForwarded = ${?OTOROSHI_OPTIONS_TRUST_XFORWARDED} - disableFunnyLogos = false - disableFunnyLogos = ${?OTOROSHI_OPTIONS_DISABLE_FUNNY_LOGOS} - staticExposedDomain = ${?OTOROSHI_OPTIONS_STATIC_EXPOSED_DOMAIN} - enable-json-media-type-with-open-charset = false # allow application/json media type with charset even if its not standard - enable-json-media-type-with-open-charset = ${?OTOROSHI_OPTIONS_ENABLE_JSON_MEDIA_TYPE_WITH_OPEN_CHARSET} - dynamicBodySizeCompute = true - dynamicBodySizeCompute = ${?OTOROSHI_OPTIONS_DYNAMIC_BODY_SIZE_COMPUTE} - } - wasm { - cache { - ttl = 10000 - ttl = ${?OTOROSHI_WASM_CACHE_TTL} - size = 100 - size = ${?OTOROSHI_WASM_CACHE_SIZE} - } - queue { - buffer { - size = 2048 - size = ${?OTOROSHI_WASM_QUEUE_BUFFER_SIZE} - } - } - } - anonymous-reporting { - enabled = true - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_ENABLED} - redirect = false - url = ${?OTOROSHI_ANONYMOUS_REPORTING_REDIRECT} - url = "https://reporting.otoroshi.io/ingest" - url = ${?OTOROSHI_ANONYMOUS_REPORTING_URL} - timeout = 60000 - timeout = ${?OTOROSHI_ANONYMOUS_REPORTING_TIMEOUT} - tls { - # certs = [] - # trustedCerts = [] - enabled = false # enable mtls - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_ENABLED} # enable mtls - loose = false # loose verification - loose = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_LOOSE} # loose verification - trustAll = false # trust any CA - trustAll = ${?OTOROSHI_ANONYMOUS_REPORTING_TLS_ALL} # trust any CA - } - proxy { - enabled = false # enable proxy - enabled = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_ENABLED} # enable proxy - host = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_HOST}, - port = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PORT}, - principal = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PRINCIPAL}, - password = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_PASSWORD}, - ntlmDomain = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_DOMAIN}, - encoding = ${?OTOROSHI_ANONYMOUS_REPORTING_PROXY_ENCODING}, - } - } - backoffice { - flags { - useAkkaHttpClient = false - useAkkaHttpClient = ${?OTOROSHI_BACKOFFICE_FLAGS_USE_AKKA_HTTP_CLIENT} - logUrl = false - logUrl = ${?OTOROSHI_BACKOFFICE_FLAGS_LOG_URL} - requestTimeout = 60000 - requestTimeout = ${?OTOROSHI_BACKOFFICE_FLAGS_REQUEST_TIMEOUT} - } - } - sessions { - secret = ${otoroshi.secret} - secret = ${?OTOROSHI_SESSIONS_SECRET} - } - cache { - enabled = false - enabled = ${?USE_CACHE} - enabled = ${?OTOROSHI_USE_CACHE} - enabled = ${?OTOROSHI_ENTITIES_CACHE_ENABLED} - ttl = 2000 - ttl = ${?OTOROSHI_ENTITIES_CACHE_TTL} - } - metrics { - enabled = true - enabled = ${?OTOROSHI_METRICS_ENABLED} - every = 30000 - every = ${?OTOROSHI_METRICS_EVERY} - accessKey = ${?app.health.accessKey} - accessKey = ${?OTOROSHI_app.health.accessKey} - accessKey = ${?OTOROSHI_METRICS_ACCESS_KEY} - } - plugins { - packages = [] - packagesStr = ${?OTOROSHI_PLUGINS_SCAN_PACKAGES} - print = false - print = ${?OTOROSHI_PLUGINS_PRINT} - blacklisted = [] - blacklistedStr = ${?OTOROSHI_PLUGINS_BACKLISTED} - } - scripts { - enabled = true # enable scripts - enabled = ${?OTOROSHI_SCRIPTS_ENABLED} # enable scripts - static { # settings for statically enabled script/plugins - enabled = false - enabled = ${?OTOROSHI_SCRIPTS_STATIC_ENABLED} - transformersRefs = [] - transformersRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_TRANSFORMER_REFS} - transformersConfig = {} - transformersConfigStr= ${?OTOROSHI_SCRIPTS_STATIC_TRANSFORMER_CONFIG} - validatorRefs = [] - validatorRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_VALIDATOR_REFS} - validatorConfig = {} - validatorConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_VALIDATOR_CONFIG} - preRouteRefs = [] - preRouteRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_PRE_ROUTE_REFS} - preRouteConfig = {} - preRouteConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_PRE_ROUTE_CONFIG} - sinkRefs = [] - sinkRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_SINK_REFS} - sinkConfig = {} - sinkConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_SINK_CONFIG} - jobsRefs = [] - jobsRefsStr = ${?OTOROSHI_SCRIPTS_STATIC_JOBS_REFS} - jobsConfig = {} - jobsConfigStr = ${?OTOROSHI_SCRIPTS_STATIC_JOBS_CONFIG} - } - } - tls = ${otoroshi.ssl} - ssl { - # the cipher suites used by otoroshi TLS termination - cipherSuitesJDK11Plus = ["TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"] - cipherSuitesJDK11 = ["TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"] - cipherSuitesJDK8 = ["TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_RSA_WITH_AES_256_CBC_SHA256", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"] - # cipherSuites = ["TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"] - cipherSuites = ${otoroshi.ssl.cipherSuitesJDK11} - # the protocols used by otoroshi TLS termination - protocolsJDK11 = ["TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1"] - protocolsJDK8 = ["SSLv2Hello", "TLSv1", "TLSv1.1", "TLSv1.2"] - modernProtocols = ["TLSv1.3", "TLSv1.2"] - protocols = ${otoroshi.ssl.modernProtocols} - # the JDK cacert access - cacert { - path = "$JAVA_HOME/lib/security/cacerts" - password = "changeit" - } - # the mtls mode - fromOutside { - clientAuth = "None" - clientAuth = ${?SSL_OUTSIDE_CLIENT_AUTH} - clientAuth = ${?OTOROSHI_SSL_OUTSIDE_CLIENT_AUTH} - netty { - clientAuth = ${?OTOROSHI_SSL_OUTSIDE_NETTY_CLIENT_AUTH} - } - } - # the default trust mode - trust { - all = false - all = ${?OTOROSHI_SSL_TRUST_ALL} - } - rootCa { - ca = ${?OTOROSHI_SSL_ROOTCA_CA} - cert = ${?OTOROSHI_SSL_ROOTCA_CERT} - key = ${?OTOROSHI_SSL_ROOTCA_KEY} - importCa = false - importCa = ${?OTOROSHI_SSL_ROOTCA_IMPORTCA} - } - # some initial cacert access, useful to include non standard CA when starting - initialCacert = ${?CLUSTER_WORKER_INITIAL_CACERT} - initialCacert = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CACERT} - initialCacert = ${?INITIAL_CACERT} - initialCacert = ${?OTOROSHI_INITIAL_CACERT} - initialCert = ${?CLUSTER_WORKER_INITIAL_CERT} - initialCert = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CERT} - initialCert = ${?INITIAL_CERT} - initialCert = ${?OTOROSHI_INITIAL_CERT} - initialCertKey = ${?CLUSTER_WORKER_INITIAL_CERT_KEY} - initialCertKey = ${?OTOROSHI_CLUSTER_WORKER_INITIAL_CERT_KEY} - initialCertKey = ${?INITIAL_CERT_KEY} - initialCertKey = ${?OTOROSHI_INITIAL_CERT_KEY} - initialCertImportCa = ${?OTOROSHI_INITIAL_CERT_IMPORTCA} - # initialCerts = [] - } - cluster { - mode = "off" # can be "off", "leader", "worker" - mode = ${?CLUSTER_MODE} # can be "off", "leader", "worker" - mode = ${?OTOROSHI_CLUSTER_MODE} # can be "off", "leader", "worker" - compression = -1 # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9 - compression = ${?CLUSTER_COMPRESSION} # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9 - compression = ${?OTOROSHI_CLUSTER_COMPRESSION} # compression of the data sent between leader cluster and worker cluster. From -1 (disabled) to 9 - retryDelay = 300 # the delay before retrying a request to leader - retryDelay = ${?CLUSTER_RETRY_DELAY} # the delay before retrying a request to leader - retryDelay = ${?OTOROSHI_CLUSTER_RETRY_DELAY} # the delay before retrying a request to leader - retryFactor = 2 # the retry factor to avoid high load on failing nodes - retryFactor = ${?CLUSTER_RETRY_FACTOR} # the retry factor to avoid high load on failing nodes - retryFactor = ${?OTOROSHI_CLUSTER_RETRY_FACTOR} # the retry factor to avoid high load on failing nodes - selfAddress = ${?CLUSTER_SELF_ADDRESS} # the instance ip address - selfAddress = ${?OTOROSHI_CLUSTER_SELF_ADDRESS} # the instance ip address - autoUpdateState = true # auto update cluster state with a job (more efficient) - autoUpdateState = ${?CLUSTER_AUTO_UPDATE_STATE} # auto update cluster state with a job (more efficient - autoUpdateState = ${?OTOROSHI_CLUSTER_AUTO_UPDATE_STATE} # auto update cluster state with a job (more efficient - backup { - enabled = false - enabled = ${?OTOROSHI_CLUSTER_BACKUP_ENABLED} - kind = "S3" - kind = ${?OTOROSHI_CLUSTER_BACKUP_KIND} - instance { - can-write = false - can-write = ${?OTOROSHI_CLUSTER_BACKUP_INSTANCE_CAN_WRITE} - can-read = false - can-read = ${?OTOROSHI_CLUSTER_BACKUP_INSTANCE_CAN_READ} - } - s3 { - bucket = ${?OTOROSHI_CLUSTER_BACKUP_S3_BUCKET} - endpoint = ${?OTOROSHI_CLUSTER_BACKUP_S3_ENDPOINT} - region = ${?OTOROSHI_CLUSTER_BACKUP_S3_REGION} - access = ${?OTOROSHI_CLUSTER_BACKUP_S3_ACCESSKEY} - secret = ${?OTOROSHI_CLUSTER_BACKUP_S3_SECRET} - path = ${?OTOROSHI_CLUSTER_BACKUP_S3_PATH} - chunk-size = ${?OTOROSHI_CLUSTER_BACKUP_S3_CHUNK_SIZE} - v4auth = ${?OTOROSHI_CLUSTER_BACKUP_S3_V4AUTH} - acl = ${?OTOROSHI_CLUSTER_BACKUP_S3_ACL} - } - } - relay { # relay routing settings - enabled = false # enable relay routing - enabled = ${?OTOROSHI_CLUSTER_RELAY_ENABLED} # enable relay routing - leaderOnly = false - leaderOnly = ${?OTOROSHI_CLUSTER_RELAY_LEADER_ONLY} # workers always pass through leader for relay routing - location { - provider = ${?otoroshi.instance.provider} - provider = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_PROVIDER} - provider = ${?app.instance.provider} - zone = ${?otoroshi.instance.zone} - zone = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_ZONE} - zone = ${?app.instance.zone} - region = ${?otoroshi.instance.region} - region = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_REGION} - region = ${?app.instance.region} - datacenter = ${?otoroshi.instance.dc} - datacenter = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_DATACENTER} - datacenter = ${?app.instance.dc} - rack = ${?otoroshi.instance.rack} - rack = ${?OTOROSHI_CLUSTER_RELAY_LOCATION_RACK} - rack = ${?app.instance.rack} - } - exposition { - url = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_URL} - urls = [] - urlsStr = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_URLS} - hostname = "otoroshi-api.oto.tools" - hostname = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_HOSTNAME} - clientId = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_CLIENT_ID} - clientSecret = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_CLIENT_SECRET} - ipAddress = ${?OTOROSHI_CLUSTER_RELAY_EXPOSITION_IP_ADDRESS} - } - } - mtls { - # certs = [] - # trustedCerts = [] - enabled = false # enable mtls - enabled = ${?CLUSTER_MTLS_ENABLED} # enable mtls - enabled = ${?OTOROSHI_CLUSTER_MTLS_ENABLED} # enable mtls - loose = false # loose verification - loose = ${?CLUSTER_MTLS_LOOSE} # loose verification - loose = ${?OTOROSHI_CLUSTER_MTLS_LOOSE} # loose verification - trustAll = false # trust any CA - trustAll = ${?CLUSTER_MTLS_TRUST_ALL} # trust any CA - trustAll = ${?OTOROSHI_CLUSTER_MTLS_TRUST_ALL} # trust any CA - } - proxy { - enabled = false # enable proxy - enabled = ${?CLUSTER_PROXY_ENABLED} # enable proxy - host = ${?CLUSTER_PROXY_HOST}, - port = ${?CLUSTER_PROXY_PORT}, - principal = ${?CLUSTER_PROXY_PRINCIPAL}, - password = ${?CLUSTER_PROXY_PASSWORD}, - ntlmDomain = ${?CLUSTER_PROXY_NTLM_DOMAIN}, - encoding = ${?CLUSTER_PROXY_ENCODING}, - } - leader { - name = ${?CLUSTER_LEADER_NAME} # the leader name - name = ${?OTOROSHI_CLUSTER_LEADER_NAME} # the leader name - urls = ["http://127.0.0.1:8080"] # the leader urls - urlsStr = ${?CLUSTER_LEADER_URLS} # the leader urls - urlsStr = ${?OTOROSHI_CLUSTER_LEADER_URLS} # the leader urls - url = ${?CLUSTER_LEADER_URL} # the leader url - url = ${?OTOROSHI_CLUSTER_LEADER_URL} # the leader url - host = "otoroshi-api.oto.tools" # the leaders api hostname - host = ${?CLUSTER_LEADER_HOST} # the leaders api hostname - host = ${?OTOROSHI_CLUSTER_LEADER_HOST} # the leaders api hostname - clientId = "admin-api-apikey-id" # the leaders apikey id to access otoroshi admin api - clientId = ${?CLUSTER_LEADER_CLIENT_ID} # the leaders apikey id to access otoroshi admin api - clientId = ${?OTOROSHI_CLUSTER_LEADER_CLIENT_ID} # the leaders apikey id to access otoroshi admin api - clientSecret = "admin-api-apikey-secret" # the leaders apikey secret to access otoroshi admin api - clientSecret = ${?CLUSTER_LEADER_CLIENT_SECRET} # the leaders apikey secret to access otoroshi admin api - clientSecret = ${?OTOROSHI_CLUSTER_LEADER_CLIENT_SECRET} # the leaders apikey secret to access otoroshi admin api - groupingBy = 50 # items grouping when streaming state - groupingBy = ${?CLUSTER_LEADER_GROUP_BY} # items grouping when streaming state - groupingBy = ${?OTOROSHI_CLUSTER_LEADER_GROUP_BY} # items grouping when streaming state - cacheStateFor = 10000 # the ttl for local state cache - cacheStateFor = ${?CLUSTER_LEADER_CACHE_STATE_FOR} # the ttl for local state cache - cacheStateFor = ${?OTOROSHI_CLUSTER_LEADER_CACHE_STATE_FOR} # the ttl for local state cache - stateDumpPath = ${?CLUSTER_LEADER_DUMP_PATH} # eventually a dump state path for debugging purpose - stateDumpPath = ${?OTOROSHI_CLUSTER_LEADER_DUMP_PATH} # eventually a dump state path for debugging purpose - } - worker { - name = ${?CLUSTER_WORKER_NAME} # the workers name - name = ${?OTOROSHI_CLUSTER_WORKER_NAME} # the workers name - retries = 3 # the number of retries when pushing quotas/pulling state - retries = ${?CLUSTER_WORKER_RETRIES} # the number of retries when pushing quotas/pulling state - retries = ${?OTOROSHI_CLUSTER_WORKER_RETRIES} # the number of retries when pushing quotas/pulling state - timeout = 10000 # the workers timeout when interacting with leaders - timeout = ${?CLUSTER_WORKER_TIMEOUT} # the workers timeout when interacting with leaders - timeout = ${?OTOROSHI_CLUSTER_WORKER_TIMEOUT} # the workers timeout when interacting with leaders - tenants = [] # the list of organization served by this worker. If none, it's all - tenantsStr = ${?CLUSTER_WORKER_TENANTS} # the list (coma separated) of organization served by this worker. If none, it's all - tenantsStr = ${?OTOROSHI_CLUSTER_WORKER_TENANTS} # the list (coma separated) of organization served by this worker. If none, it's all - dbpath = ${?CLUSTER_WORKER_DB_PATH} # state dump path for debugging purpose - dbpath = ${?OTOROSHI_CLUSTER_WORKER_DB_PATH} # state dump path for debugging purpose - dataStaleAfter = 600000 # the amount of time needed to consider state is stale - dataStaleAfter = ${?CLUSTER_WORKER_DATA_STALE_AFTER} # the amount of time needed to consider state is stale - dataStaleAfter = ${?OTOROSHI_CLUSTER_WORKER_DATA_STALE_AFTER} # the amount of time needed to consider state is stale - swapStrategy = "Merge" # the internal memory store strategy, can be Replace or Merge - swapStrategy = ${?CLUSTER_WORKER_SWAP_STRATEGY} # the internal memory store strategy, can be Replace or Merge - swapStrategy = ${?OTOROSHI_CLUSTER_WORKER_SWAP_STRATEGY} # the internal memory store strategy, can be Replace or Merge - modern = false # use a modern store implementation - modern = ${?CLUSTER_WORKER_STORE_MODERN} - modern = ${?OTOROSHI_CLUSTER_WORKER_STORE_MODERN} - useWs = false - useWs = ${?CLUSTER_WORKER_USE_WS} - useWs = ${?OTOROSHI_CLUSTER_WORKER_USE_WS} - state { - retries = ${otoroshi.cluster.worker.retries} # the number of retries when pulling state - retries = ${?CLUSTER_WORKER_STATE_RETRIES} # the number of retries when pulling state - retries = ${?OTOROSHI_CLUSTER_WORKER_STATE_RETRIES} # the number of retries when pulling state - pollEvery = 10000 # polling interval - pollEvery = ${?CLUSTER_WORKER_POLL_EVERY} # polling interval - pollEvery = ${?OTOROSHI_CLUSTER_WORKER_POLL_EVERY} # polling interval - timeout = ${otoroshi.cluster.worker.timeout} # the workers timeout when polling state - timeout = ${?CLUSTER_WORKER_POLL_TIMEOUT} # the workers timeout when polling state - timeout = ${?OTOROSHI_CLUSTER_WORKER_POLL_TIMEOUT} # the workers timeout when polling state - } - quotas { - retries = ${otoroshi.cluster.worker.retries} # the number of retries when pushing quotas - retries = ${?CLUSTER_WORKER_QUOTAS_RETRIES} # the number of retries when pushing quotas - retries = ${?OTOROSHI_CLUSTER_WORKER_QUOTAS_RETRIES} # the number of retries when pushing quotas - pushEvery = 10000 # pushing interval - pushEvery = ${?CLUSTER_WORKER_PUSH_EVERY} # pushing interval - pushEvery = ${?OTOROSHI_CLUSTER_WORKER_PUSH_EVERY} # pushing interval - timeout = ${otoroshi.cluster.worker.timeout} # the workers timeout when pushing quotas - timeout = ${?CLUSTER_WORKER_PUSH_TIMEOUT} # the workers timeout when pushing quotas - timeout = ${?OTOROSHI_CLUSTER_WORKER_PUSH_TIMEOUT} # the workers timeout when pushing quotas - } - } - analytics { # settings for the analytics actor system which is separated from otoroshi default one for performance reasons - pressure { - enabled = true - enabled = ${?OTOROSHI_ANALYTICS_PRESSURE_ENABLED} - } - actorsystem { - akka { - version = ${akka.version} - log-dead-letters-during-shutdown = false - jvm-exit-on-fatal-error = false - default-dispatcher { - type = Dispatcher - executor = "fork-join-executor" - fork-join-executor { - parallelism-factor = 4.0 - parallelism-min = 4 - parallelism-max = 64 - task-peeking-mode = "FIFO" - } - throughput = 1 - } - # http { - # parsing { - # max-uri-length = 4k - # max-method-length = 16 - # max-response-reason-length = 64 - # max-header-name-length = 128 - # max-header-value-length = 16k - # max-header-count = 128 - # max-chunk-ext-length = 256 - # max-chunk-size = 256m - # max-chunk-size = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_CHUNK_SIZE} - # max-chunk-size = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_CHUNK_SIZE} - # max-content-length = infinite - # max-content-length = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_CONTENT_LENGHT} - # max-content-length = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_CONTENT_LENGHT} - # max-to-strict-bytes = infinite - # max-to-strict-bytes = ${?AKKA_HTTP_CLIENT_ANALYTICS_MAX_TO_STRICT_BYTES} - # max-to-strict-bytes = ${?OTOROSHI_AKKA_HTTP_CLIENT_ANALYTICS_MAX_TO_STRICT_BYTES} - # } - # } - } - } - } - } - headers { # the default headers value for specific otoroshi headers - trace.label = "Otoroshi-Viz-From-Label" - trace.from = "Otoroshi-Viz-From" - trace.parent = "Otoroshi-Parent-Request" - request.adminprofile = "Otoroshi-Admin-Profile" - request.simpleapiclientid = "x-api-key" - request.clientid = "Otoroshi-Client-Id" - request.clientsecret = "Otoroshi-Client-Secret" - request.id = "Otoroshi-Request-Id" - request.timestamp = "Otoroshi-Request-Timestamp" - request.bearer = "Otoroshi-Token" - request.authorization = "Otoroshi-Authorization" - response.proxyhost = "Otoroshi-Proxied-Host" - response.error = "Otoroshi-Error" - response.errormsg = "Otoroshi-Error-Msg" - response.errorcause = "Otoroshi-Error-Cause" - response.proxylatency = "Otoroshi-Proxy-Latency" - response.upstreamlatency = "Otoroshi-Upstream-Latency" - response.dailyquota = "Otoroshi-Daily-Calls-Remaining" - response.monthlyquota = "Otoroshi-Monthly-Calls-Remaining" - comm.state = "Otoroshi-State" - comm.stateresp = "Otoroshi-State-Resp" - comm.claim = "Otoroshi-Claim" - healthcheck.test = "Otoroshi-Health-Check-Logic-Test" - healthcheck.testresult = "Otoroshi-Health-Check-Logic-Test-Result" - jwt.issuer = "Otoroshi" - canary.tracker = "Otoroshi-Canary-Id" - client.cert.chain = "Otoroshi-Client-Cert-Chain" - - request.jwtAuthorization = "access_token" - request.bearerAuthorization = "bearer_auth" - request.basicAuthorization = "basic_auth" - } - requests { - validate = true - validate = ${?OTOROSHI_REQUESTS_VALIDATE} - maxUrlLength = ${akka.http.parsing.max-uri-length} - maxCookieLength = ${akka.http.parsing.max-header-value-length} - maxHeaderNameLength = ${akka.http.parsing.max-header-name-length} - maxHeaderValueLength = ${akka.http.parsing.max-header-value-length} - } - jmx { - enabled = false - enabled = ${?OTOROSHI_JMX_ENABLED} - port = 16000 - port = ${?OTOROSHI_JMX_PORT} - } - loggers { - } - provider { - dashboardUrl = ${?OTOROSHI_PROVIDER_DASHBOARD_URL} - jsUrl = ${?OTOROSHI_PROVIDER_JS_URL} - cssUrl = ${?OTOROSHI_PROVIDER_CSS_URL} - secret = "secret" - secret = ${?OTOROSHI_PROVIDER_SECRET} - title = "Provider's dashboard" - title = ${?OTOROSHI_PROVIDER_TITLE} - } - healthcheck { - workers = 4 - workers = ${?OTOROSHI_HEALTHCHECK_WORKERS} - block-on-red = false - block-on-red = ${?OTOROSHI_HEALTHCHECK_BLOCK_ON_RED} - block-on-red = ${?OTOROSHI_HEALTHCHECK_BLOCK_ON_500} - ttl = 60000 - ttl = ${?OTOROSHI_HEALTHCHECK_TTL} - ttl-only = true - ttl-only = ${?OTOROSHI_HEALTHCHECK_TTL_ONLY} - } - vaults { - enabled = false - enabled = ${?OTOROSHI_VAULTS_ENABLED} - secrets-ttl = 300000 # 5 minutes between each secret read - secrets-ttl = ${?OTOROSHI_VAULTS_SECRETS_TTL} - secrets-error-ttl = 20000 # wait 20000 before retrying on error - secrets-error-ttl = ${?OTOROSHI_VAULTS_SECRETS_ERROR_TTL} - cached-secrets = 10000 - cached-secrets = ${?OTOROSHI_VAULTS_CACHED_SECRETS} - read-ttl = 10000 # 10 seconds - read-timeout = ${?otoroshi.vaults.read-ttl} - read-timeout = ${?OTOROSHI_VAULTS_READ_TTL} - read-timeout = ${?OTOROSHI_VAULTS_READ_TIMEOUT} - parallel-fetchs = 4 - parallel-fetchs = ${?OTOROSHI_VAULTS_PARALLEL_FETCHS} - # if enabled, only leader nodes fetches the secrets. - # entities with secret values filled are then sent to workers when they poll the cluster state. - # only works if `otoroshi.cluster.autoUpdateState=true` - leader-fetch-only = false - leader-fetch-only = ${?OTOROSHI_VAULTS_LEADER_FETCH_ONLY} - env { - type = "env" - prefix = ${?OTOROSHI_VAULTS_ENV_PREFIX} - } - local { - type = "local" - root = ${?OTOROSHI_VAULTS_LOCAL_ROOT} - } - # hashicorpvault { - # type = "hashicorp-vault" - # url = "http://127.0.0.1:8200" - # mount = "kv" - # kv = "v2" - # token = "root" - # } - } - tunnels { - enabled = true - enabled = ${?OTOROSHI_TUNNELS_ENABLED} - worker-ws = true - worker-ws = ${?OTOROSHI_TUNNELS_WORKER_WS} - worker-use-internal-ports = false - worker-use-internal-ports = ${?OTOROSHI_TUNNELS_WORKER_USE_INTERNAL_PORTS} - worker-use-loadbalancing = false - worker-use-loadbalancing = ${?OTOROSHI_TUNNELS_WORKER_USE_LOADBALANCING} - default { - enabled = false - enabled = ${?OTOROSHI_TUNNELS_DEFAULT_ENABLED} - id = "default" - id = ${?OTOROSHI_TUNNELS_DEFAULT_ID} - name = "default" - name = ${?OTOROSHI_TUNNELS_DEFAULT_NAME} - url = "http://127.0.0.1:8080" - url = ${?OTOROSHI_TUNNELS_DEFAULT_URL} - host = "otoroshi-api.oto.tools" - host = ${?OTOROSHI_TUNNELS_DEFAULT_HOST} - clientId = "admin-api-apikey-id" - clientId = ${?OTOROSHI_TUNNELS_DEFAULT_CLIENT_ID} - clientSecret = "admin-api-apikey-secret" - clientSecret = ${?OTOROSHI_TUNNELS_DEFAULT_CLIENT_SECRET} - export-routes = true # send routes information to remote otoroshi instance to facilitate remote route exposition - export-routes = ${?OTOROSHI_TUNNELS_DEFAULT_EXPORT_ROUTES} # send routes information to remote otoroshi instance to facilitate remote route exposition - export-routes-tag = ${?OTOROSHI_TUNNELS_DEFAULT_EXPORT_TAG} # only send routes information if the route has this tag - proxy { - enabled = false - host = none - port = none - principal = none - password = none - nonProxyHosts = [] - } - } - } - admin-extensions { - enabled = true - enabled = ${?OTOROSHI_ADMIN_EXTENSIONS_ENABLED} - configurations { - otoroshi_extensions_foo { - enabled = false - } - otoroshi_extensions_greenscore { - enabled = false - enabled = ${?OTOROSHI_ADMIN_EXTENSIONS_GREENSCORE_ENABLED} - } - } - } -} - - -http.port = 8080 # the main http port for the otoroshi server -http.port = ${?otoroshi.http.port} # the main http port for the otoroshi server -http.port = ${?PORT} # the main http port for the otoroshi server -http.port = ${?OTOROSHI_PORT} # the main http port for the otoroshi server -http.port = ${?OTOROSHI_HTTP_PORT} # the main http port for the otoroshi server -play.server.http.port = ${http.port} # the main http port for the otoroshi server -play.server.http.port = ${?PORT} # the main http port for the otoroshi server -play.server.http.port = ${?OTOROSHI_PORT} # the main http port for the otoroshi server -play.server.http.port = ${?OTOROSHI_HTTP_PORT} # the main http port for the otoroshi server -https.port = 8443 # the main https port for the otoroshi server -https.port = ${?otoroshi.https.port} # the main https port for the otoroshi server -https.port = ${?HTTPS_PORT} # the main https port for the otoroshi server -https.port = ${?OTOROSHI_HTTPS_PORT} # the main https port for the otoroshi server - -play.server.https.engineProvider = "otoroshi.ssl.DynamicSSLEngineProvider" # the module to handle TLS connections dynamically -play.server.https.keyStoreDumpPath = ${?HTTPS_KEYSTORE_DUMP_PATH} # the file path where the TLSContext will be dumped (for debugging purposes only) -play.server.https.keyStoreDumpPath = ${?OTOROSHI_HTTPS_KEYSTORE_DUMP_PATH} # the file path where the TLSContext will be dumped (for debugging purposes only) - -play.http.secret.key = ${otoroshi.secret} # the secret used to signed session cookies -play.http.secret.key = ${?PLAY_CRYPTO_SECRET} # the secret used to signed session cookies -play.http.secret.key = ${?OTOROSHI_CRYPTO_SECRET} # the secret used to signed session cookies - -play.server.http.idleTimeout = 3600s # the default server idle timeout -play.server.http.idleTimeout = ${?PLAY_SERVER_IDLE_TIMEOUT} # the default server idle timeout -play.server.http.idleTimeout = ${?OTOROSHI_SERVER_IDLE_TIMEOUT} # the default server idle timeout -play.server.akka.requestTimeout = 3600s # the default server idle timeout (for akka server specifically) -play.server.akka.requestTimeout = ${?PLAY_SERVER_REQUEST_TIMEOUT} # the default server idle timeout (for akka server specifically) -play.server.akka.requestTimeout = ${?OTOROSHI_SERVER_REQUEST_TIMEOUT} # the default server idle timeout (for akka server specifically) - -http2.enabled = true # enable HTTP2 support -http2.enabled = ${?otoroshi.http2.enabled} -http2.enabled = ${?HTTP2_ENABLED} # enable HTTP2 support -http2.enabled = ${?OTOROSHI_HTTP2_ENABLED} # enable HTTP2 support - -play.server.https.keyStore.path=${?HTTPS_KEYSTORE_PATH} # settings for the default server keystore -play.server.https.keyStore.path=${?OTOROSHI_HTTPS_KEYSTORE_PATH} # settings for the default server keystore -play.server.https.keyStore.type=${?HTTPS_KEYSTORE_TYPE} # settings for the default server keystore -play.server.https.keyStore.type=${?OTOROSHI_HTTPS_KEYSTORE_TYPE} # settings for the default server keystore -play.server.https.keyStore.password=${?HTTPS_KEYSTORE_PASSWORD} # settings for the default server keystore -play.server.https.keyStore.password=${?OTOROSHI_HTTPS_KEYSTORE_PASSWORD} # settings for the default server keystore -play.server.https.keyStore.algorithm=${?HTTPS_KEYSTORE_ALGO} # settings for the default server keystore -play.server.https.keyStore.algorithm=${?OTOROSHI_HTTPS_KEYSTORE_ALGO} # settings for the default server keystore - - -play.server.websocket.frame.maxLength = 1024k -play.server.websocket.frame.maxLength = ${?OTOROSHI_WEBSOCKET_FRAME_MAX_LENGTH} - - - -play.application.loader = "otoroshi.loader.OtoroshiLoader" # the loader used to launch otoroshi - -play.http { - session { - secure = false # the cookie for otoroshi backoffice should be exhanged over https only - secure = ${?SESSION_SECURE_ONLY} # the cookie for otoroshi backoffice should be exhanged over https only - secure = ${?OTOROSHI_SESSION_SECURE_ONLY} # the cookie for otoroshi backoffice should be exhanged over https only - httpOnly = true # the cookie for otoroshi backoffice is not accessible from javascript - maxAge = 259200000 # the cookie for otoroshi backoffice max age - maxAge = ${?SESSION_MAX_AGE} # the cookie for otoroshi backoffice max age - maxAge = ${?OTOROSHI_SESSION_MAX_AGE} # the cookie for otoroshi backoffice max age - # domain = "."${?app.domain} # the cookie for otoroshi backoffice domain - domain = "."${otoroshi.domain} # the cookie for otoroshi backoffice domain - domain = ${?SESSION_DOMAIN} # the cookie for otoroshi backoffice domain - domain = ${?OTOROSHI_SESSION_DOMAIN} # the cookie for otoroshi backoffice domain - cookieName = "otoroshi-session" # the cookie for otoroshi backoffice name - cookieName = ${?SESSION_NAME} # the cookie for otoroshi backoffice name - cookieName = ${?OTOROSHI_SESSION_NAME} # the cookie for otoroshi backoffice name - } -} - - - - - - -akka { # akka specific configuration - version = "2.6.20" - loglevel = ERROR - logger-startup-timeout = 60s - log-dead-letters-during-shutdown = false - jvm-exit-on-fatal-error = false - actor { - default-dispatcher { - type = Dispatcher - executor = "fork-join-executor" - fork-join-executor { - parallelism-factor = 4.0 - parallelism-factor = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_FACTOR} - parallelism-min = 8 - parallelism-min = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_MIN} - parallelism-max = 64 - parallelism-max = ${?OTOROSHI_AKKA_DISPATCHER_PARALLELISM_MAX} - task-peeking-mode = "FIFO" - task-peeking-mode = ${?OTOROSHI_AKKA_DISPATCHER_TASK_PEEKING_MODE} - } - throughput = 1 - throughput = ${?OTOROSHI_AKKA_DISPATCHER_THROUGHPUT} - } - } - http { - server { - server-header = otoroshi - max-connections = 2048 - max-connections = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CONNECTIONS} - remote-address-header = on - raw-request-uri-header = on - pipelining-limit = 64 - pipelining-limit = ${?OTOROSHI_AKKA_HTTP_SERVER_PIPELINING_LIMIT} - backlog = 512 - backlog = ${?OTOROSHI_AKKA_HTTP_SERVER_BACKLOG} - socket-options { - so-receive-buffer-size = undefined - so-send-buffer-size = undefined - so-reuse-address = undefined - so-traffic-class = undefined - tcp-keep-alive = true - tcp-oob-inline = undefined - tcp-no-delay = undefined - } - http2 { - request-entity-chunk-size = 65536 b - incoming-connection-level-buffer-size = 10 MB - incoming-stream-level-buffer-size = 512kB - } - } - client { - user-agent-header = Otoroshi-akka - socket-options { - so-receive-buffer-size = undefined - so-send-buffer-size = undefined - so-reuse-address = undefined - so-traffic-class = undefined - tcp-keep-alive = true - tcp-oob-inline = undefined - tcp-no-delay = undefined - } - } - host-connection-pool { - max-connections = 512 - max-connections = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_MAX_CONNECTIONS} - max-open-requests = 2048 - max-open-requests = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_MAX_OPEN_REQUESTS} - pipelining-limit = 32 - pipelining-limit = ${?OTOROSHI_AKKA_HTTP_SERVER_HOST_CONNECTION_POOL_PIPELINING_LIMIT} - client { - user-agent-header = otoroshi - socket-options { - so-receive-buffer-size = undefined - so-send-buffer-size = undefined - so-reuse-address = undefined - so-traffic-class = undefined - tcp-keep-alive = true - tcp-oob-inline = undefined - tcp-no-delay = undefined - } - } - } - parsing { - max-uri-length = 4k - max-uri-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_URI_LENGTH} - max-method-length = 16 - max-method-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_METHOD_LENGTH} - max-response-reason-length = 128 - max-response-reason-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_RESPONSE_REASON_LENGTH} - max-header-name-length = 128 - max-header-name-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_NAME_LENGTH} - max-header-value-length = 16k - max-header-value-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_VALUE_LENGTH} - max-header-count = 128 - max-header-count = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_HEADER_COUNT} - max-chunk-ext-length = 256 - max-chunk-ext-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CHUNK_EXT_LENGTH} - max-chunk-size = 256m - max-chunk-size = ${?AKKA_HTTP_SERVER_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CHUNK_SIZE} - max-chunk-size = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CHUNK_SIZE} - max-content-length = infinite - max-content-length = ${?AKKA_HTTP_SERVER_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_SERVER_MAX_CONTENT_LENGHT} - max-content-length = ${?OTOROSHI_AKKA_HTTP_SERVER_PARSING_MAX_CONTENT_LENGHT} - } - } -} \ No newline at end of file

      |?Y9fsI7SFh094VQU;u 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 Sg;^|Y`~LwC`VquN{r~`vKZ)P~ diff --git a/docs/devmanual/search/index/en_a5f29ea.pf_index b/docs/devmanual/search/index/en_a5f29ea.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..6d1c5ce793d44bb6d1585b1b1e1ceb092c510877 GIT binary patch literal 2802 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�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 D&0L5H literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_ae3d1a7.pf_index b/docs/devmanual/search/index/en_ae3d1a7.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..004db8fbb97d104881a4ca131781b9b01135ca77 GIT binary patch literal 40404 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 UZsjFa1K%D0FO39>E%yBY02#z6XaE2J literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_b0eaf31.pf_index b/docs/devmanual/search/index/en_b0eaf31.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..64e284690b2b10c17feee22d0961e513f4c52592 GIT binary patch literal 43102 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 literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_b2a4f87.pf_index b/docs/devmanual/search/index/en_b2a4f87.pf_index deleted file mode 100644 index a27a3f11e6634e600d060fc322cb3fe89f4c289e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43699 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 z 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 diff --git a/docs/devmanual/search/index/en_bdf10a3.pf_index b/docs/devmanual/search/index/en_bdf10a3.pf_index new file mode 100644 index 0000000000000000000000000000000000000000..6b09a4dd986b7ef7b52dd9d28c21250312a02aa4 GIT binary patch literal 41858 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 SFr!4@{{I0d52tC!2>}4&l!ZzF literal 0 HcmV?d00001 diff --git a/docs/devmanual/search/index/en_c919a42.pf_index b/docs/devmanual/search/index/en_c919a42.pf_index deleted file mode 100644 index f4f55d137ee4219f840fae8762a64cfa664dabfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40500 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 z 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⁡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