API gateway

The open source, self-hosted API gateway

Octelium is a free and open source, self-hosted zero trust access platform. Its HTTP and gRPC modes put an identity-aware proxy in front of your APIs: each request is authorized on its own method, path and body, the upstream credential stays in the Cluster, and plugins handle rate limiting, validation and manipulation.

  • Free and open source
  • Designed for self-hosting
  • REST and gRPC

Every request from a human, a browser, a CLI, a workload or an AI agent is authenticated against your identity providers, authorized per request at the application layer by the Octelium Service in front of the resource, exported to your SIEM and observability stack whether it is allowed or denied, and only then routed to the upstream HTTP API, database, SSH server, Kubernetes cluster, LLM or MCP server.

Signing in once, then reaching Services with and without a client

  • A browser opens grafana.acme.com and is redirected to the Cluster sign-in page at acme.com/login.
  • jane signs in with a passkey verified by a biometric on the device, and the browser returns to the Grafana dashboard at grafana.acme.com with no client installed.
  • In a terminal, octelium connect -d opens the browser at acme.com/callback/success/approval, where the same browser session approves the login with one click.
  • ~ ❯ octelium connect -d
  • Please authenticate yourself using Octelium web Portal
  • Waiting for you to approve the login in your browser. Press Ctrl-C to cancel.
  • You are now authenticated as jane
  • Octelium has started running in detached mode.
  • ~ ❯ curl -s my-api/v1/users/42 | jq
  • {
  • "id": 42,
  • "name": "Jane Doe",
  • "email": "jane@acme.com"
  • }
  • ~ ❯ psql -h pg-prod -d shop
  • psql (17.4)
  • Type "help" for help.
  • shop=> SELECT * FROM orders LIMIT 3;
  • id | status | total
  • -----+---------+--------
  • 981 | paid | 129.00
  • 982 | shipped | 48.50
  • 983 | paid | 310.25
  • (3 rows)
  • shop=> \q
  • ~ ❯ ssh bastion
  • Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-1021-aws x86_64)

Requests authorized one by one against CEL rules in the HTTP, LLM and Kubernetes modes

  • HTTP: the HTTP Service billing is configured as follows.
  • kind: Service
  • metadata:
  • name: billing
  • spec:
  • mode: HTTP
  • config:
  • authorization:
  • inlinePolicies:
  • - spec:
  • rules:
  • - effect: ALLOW
  • condition:
  • all:
  • of:
  • - match: '"finance" in ctx.user.spec.groups'
  • - match: ctx.request.http.method == "GET"
  • - match: ctx.request.http.path.startsWith("/invoices")
  • jane, a member of the finance Group, runs curl -i billing/invoices/42 and gets HTTP/1.1 200 OK Content-Type: application/json {"id":42,"status":"paid","total":1290}. The access log records ALLOWED GET /invoices/42 with POLICY_MATCH.
  • jane, a member of the finance Group, runs curl -i -X DELETE billing/invoices/42 and gets HTTP/1.1 403 Forbidden X-Octelium-Unauthorized: true Content-Length: 0. The access log records DENIED DELETE /invoices/42 with NO_POLICY_MATCH.
  • LLM: the LLM Service openai is configured as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • authorization:
  • inlinePolicies:
  • - spec:
  • rules:
  • - effect: ALLOW
  • condition:
  • any:
  • of:
  • - match: '"ai-admins" in ctx.user.spec.groups'
  • - all:
  • of:
  • - match: '"ai-agents" in ctx.user.spec.groups'
  • - match: ctx.request.llm.operation == "GENERATE"
  • - match: ctx.request.llm.model == "gpt-5-mini"
  • support-bot, a member of the ai-agents Group, runs curl openai/v1/responses --json '{"model":"gpt-5-mini","input":"Hello"}' and gets { "id": "resp_68e8e1f0c4d0", "object": "response", "created_at": 1791628935, "status": "completed", "model": "gpt-5-mini-2025-08-07",. The access log records ALLOWED RESPONSES gpt-5-mini with POLICY_MATCH.
  • support-bot, a member of the ai-agents Group, runs curl openai/v1/responses --json '{"model":"gpt-5","input":"Hello"}' and gets {"error":{"message":"Octelium: unauthorized request","type":"permission_error","param":null,"code":"octelium_unauthorized"}}. The access log records DENIED RESPONSES gpt-5 with NO_POLICY_MATCH.
  • Kubernetes: the KUBERNETES Service k8s-prod is configured as follows.
  • kind: Service
  • metadata:
  • name: k8s-prod
  • spec:
  • mode: KUBERNETES
  • config:
  • authorization:
  • inlinePolicies:
  • - spec:
  • rules:
  • - effect: ALLOW
  • condition:
  • all:
  • of:
  • - match: '"sre-agents" in ctx.user.spec.groups'
  • - match: ctx.request.kubernetes.verb in ["get", "list"]
  • - match: ctx.request.kubernetes.namespace == "production"
  • - not: ctx.request.kubernetes.resource == "secrets"
  • ops-agent, a member of the sre-agents Group, runs kubectl get configmaps -n production and gets NAME DATA AGE app-config 4 12d feature-flags 2 3d kube-root-ca.crt 1 12d. The access log records ALLOWED list configmaps · production with POLICY_MATCH.
  • ops-agent, a member of the sre-agents Group, runs kubectl get secrets -n production and gets Error from server (Forbidden): Octelium: Unauthorized request. The access log records DENIED list secrets · production with NO_POLICY_MATCH.

Upstream credentials injected per request by identity in the HTTP, SSH and PostgreSQL modes

  • HTTP: the HTTP Service stripe proxies https://api.stripe.com and picks the upstream credential of every request with dynamic configuration rules.
  • elena, a member of the finance-eu Group, runs curl -s stripe/v1/account | jq '{id, country}' without holding any credential. The rule "finance-eu" in ctx.user.spec.groups selects the eu config, so Octelium injects the Secret stripe-eu and the upstream authenticates the request as acct_1Pq8EuGmbH2bXvK3. The terminal shows { "id": "acct_1Pq8EuGmbH2bXvK3", "country": "DE" }
  • pay-agent, a member of the finance-us Group, runs curl -s stripe/v1/account | jq '{id, country}' without holding any credential. The rule "finance-us" in ctx.user.spec.groups selects the us config, so Octelium injects the Secret stripe-us and the upstream authenticates the request as acct_1Nf2UsQ7cLm4RtW9. The terminal shows { "id": "acct_1Nf2UsQ7cLm4RtW9", "country": "US" }
  • SSH: the SSH Service web-01 proxies ssh://10.0.4.21 and picks the upstream credential of every request with dynamic configuration rules.
  • sam, a member of the sre Group, runs ssh web-01 without holding any credential. The rule "sre" in ctx.user.spec.groups selects the root config, so Octelium injects the Secret root-key and the upstream authenticates the request as root. The terminal shows Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.14.0-1015-aws x86_64) root@web-01:~# id uid=0(root) gid=0(root) groups=0(root)
  • noah, a member of the developers Group, runs ssh web-01 without holding any credential. The rule "developers" in ctx.user.spec.groups selects the deploy config, so Octelium injects the Secret deploy-key and the upstream authenticates the request as deploy. The terminal shows Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.14.0-1015-aws x86_64) deploy@web-01:~$ id uid=1001(deploy) gid=1001(deploy) groups=1001(deploy)
  • PostgreSQL: the POSTGRES Service orders-db proxies postgres://10.0.2.8:5432 and picks the upstream credential of every request with dynamic configuration rules.
  • priya, a member of the backend Group, runs psql -h orders-db orders without holding any credential. The rule "backend" in ctx.user.spec.groups selects the rw config, so Octelium injects the Secret pg-rw and the upstream authenticates the request as app_rw. The terminal shows psql (17.2) Type "help" for help. orders=> UPDATE orders SET status = 'shipped' WHERE id = 42; UPDATE 1
  • bi-agent, a member of the analytics Group, runs psql -h orders-db orders without holding any credential. The rule "analytics" in ctx.user.spec.groups selects the ro config, so Octelium injects the Secret pg-ro and the upstream authenticates the request as app_ro. The terminal shows psql (17.2) Type "help" for help. orders=> UPDATE orders SET status = 'shipped' WHERE id = 42; ERROR: permission denied for table orders

Signing in with a passkey and opening the Portal

  • The sign-in page of the Cluster at acme.com/login offers login with Okta, with GitHub, or with a passkey.
  • jane@acme.com chooses Login with a Passkey and verifies it with a biometric on the device through native WebAuthn.
  • The browser is redirected to the Portal at portal.acme.com/services, which lists the Services the User can access:
  • grafana.monitoring: Grafana dashboards, Web App, Namespace monitoring.
  • aws-postgres.production: Amazon RDS for PostgreSQL, PostgreSQL, Namespace production.
  • k8s-prod: Production EKS cluster, Kubernetes, Namespace default.
  • bastion.production: SSH bastion host, SSH, Namespace production.
  • github-mcp: GitHub MCP server, MCP, Namespace default.
  • openai: OpenAI API, AI / LLM, Namespace default.
Visibility and auditing

Identity-aware visibility at the application layer

Emit structured, application-layer access logs in real time and export them through OpenTelemetry OTLP to the observability, analytics and SIEM systems your organization already operates.

Read about visibility and auditing
AccessLogOTLP
ALLOWlinus@acme.compg-prod.dbPOSTGRESquery: select id, email from users limit 50
ALLOWci-runnerprod-k8sKUBERNETESverb=list resource=pods namespace=production
DENYagent-07tools-mcpMCPtools/call name=transfer policy=mcp-tools
ALLOWjane@acme.combastionSSHsession recorded, upstream user=deploy
ALLOWagent-07openaiLLMCHAT_COMPLETIONS model=gpt-5-mini tokens=1284

Cluster resources applied declaratively from YAML with octeliumctl and managed programmatically with the Go, TypeScript, Python and Rust SDKs

  • GitOps: cluster.yaml in the main branch of the ~/octelium repository describes the desired state of the Cluster. Its diff reads as follows, with + for added, ~ for changed and - for removed lines.
  • + kind: Group
  • + metadata:
  • + name: payments
  • + spec: {}
  • + ---
  • + kind: User
  • + metadata:
  • + name: alice
  • + spec:
  • + type: HUMAN
  • + email: alice@acme.com
  • + groups: ["payments"]
  • ---
  • kind: User
  • metadata:
  • name: bob
  • spec:
  • type: HUMAN
  • ~ groups: ["payments"]
  • ---
  • kind: Service
  • metadata:
  • name: billing
  • spec:
  • mode: HTTP
  • config:
  • upstream:
  • ~ url: http://10.0.3.24:8080
  • - ---
  • - kind: Service
  • - metadata:
  • - name: billing-legacy
  • - spec: ⋯
  • - ---
  • - kind: User
  • - metadata:
  • - name: john
  • - spec: ⋯
  • ~/octelium ❯ octeliumctl apply --prune .
  • Group: payments Created
  • User: alice Created
  • User: bob Updated
  • Service: billing.default Updated
  • Service: billing-legacy.default Deleted
  • User: john Deleted
  • Cluster Core resources successfully applied
  • 2 resources created
  • 2 resources updated
  • 2 resources deleted
  • Go: main.go uses the octelium-go SDK to list the Users of the Cluster and update the email and Policies of linus.
  • client, err := octelium.New(ctx)
  • if err != nil {⋯}
  • conn, err := client.Conn(ctx)
  • if err != nil {⋯}
  • api := corev1.NewMainServiceClient(conn)
  • users, err := api.ListUser(ctx, &corev1.ListUserOptions{})
  • if err != nil {⋯}
  • for _, u := range users.Items {
  • fmt.Println(u.Metadata.Name)
  • }
  • user, err := api.GetUser(ctx, &metav1.GetOptions{Name: "linus"})
  • if err != nil {⋯}
  • user.Spec.Email = "linus@acme.com"
  • user.Spec.Authorization = &corev1.User_Spec_Authorization{
  • Policies: []string{"db-readonly"},
  • }
  • if _, err := api.UpdateUser(ctx, user); err != nil {⋯}
  • fmt.Println("updated", user.Metadata.Name)
  • ~/app ❯ go run .
  • alice
  • bob
  • linus
  • updated linus
  • TypeScript: main.ts uses the @octelium/sdk SDK to list the Users of the Cluster and update the email and Policies of linus.
  • const client = await OcteliumClient.create({
  • domain: "acme.com",
  • auth: {⋯},
  • });
  • const users = await client.coreV1.listUser(ListUserOptions.create());
  • for (const u of users.response.items) {
  • console.log(u.metadata?.name);
  • }
  • const { response: user } = await client.coreV1.getUser(
  • GetOptions.create({ name: "linus" }),
  • );
  • user.spec!.email = "linus@acme.com";
  • user.spec!.authorization = User_Spec_Authorization.create({
  • policies: ["db-readonly"],
  • });
  • await client.coreV1.updateUser(user);
  • console.log("updated", user.metadata?.name);
  • await client.close();
  • ~/app ❯ npx tsx main.ts
  • alice
  • bob
  • linus
  • updated linus
  • Python: main.py uses the octelium-sdk SDK to list the Users of the Cluster and update the email and Policies of linus.
  • async with await OcteliumClient.create() as client:
  • users = await client.core_v1.list_user(ListUserOptions())
  • for u in users.items:
  • print(u.metadata.name)
  • user = await client.core_v1.get_user(GetOptions(name="linus"))
  • user.spec.email = "linus@acme.com"
  • user.spec.authorization.policies = ["db-readonly"]
  • await client.core_v1.update_user(user)
  • print("updated", user.metadata.name)
  • ~/app ❯ python main.py
  • alice
  • bob
  • linus
  • updated linus
  • Rust: main.rs uses the octelium SDK to list the Users of the Cluster and update the email and Policies of linus.
  • let client = Client::from_env().await?;
  • let mut api = client.core_v1();
  • let users = api.list_user(ListUserOptions::default()).await?;
  • for u in users.into_inner().items {
  • println!("{}", u.metadata.unwrap_or_default().name);
  • }
  • let options = GetOptions {
  • name: "linus".into(),
  • ..Default::default()
  • };
  • let mut user = api.get_user(options).await?.into_inner();
  • let spec = user.spec.get_or_insert_default();
  • spec.email = "linus@acme.com".into();
  • spec.authorization.get_or_insert_default().policies = vec!["db-readonly".into()];
  • let user = api.update_user(user).await?.into_inner();
  • println!("updated {}", user.metadata.unwrap_or_default().name);
  • ~/app ❯ cargo run -q
  • alice
  • bob
  • linus
  • updated linus
Responsibilities

What an API gateway has to do, and what does each part

An API gateway is usually adopted for one reason and then asked to carry several others. These are the responsibilities that accumulate, next to the mechanism in an Octelium Cluster that handles each one.

  1. 01

    Knowing who is calling

    Every request carries an Octelium identity rather than a shared API key. Applications authenticate with the standard OAuth2 client credentials flow, access tokens or federated OIDC assertions, in any language and with no proprietary SDK.

    Read more
  2. 02

    Deciding per request

    Authorization runs on every request with CEL or Open Policy Agent, over the method, path, headers, query parameters and serialized JSON body, combined with identity, group membership and device attributes in the same expression.

    Read more
  3. 03

    Holding the upstream credential

    API keys, bearer tokens, basic authentication, OAuth2 client credentials, AWS SigV4 signing and mTLS client certificates are stored as Secrets and injected after a request is authorized, so callers never hold them.

    Read more
  4. 04

    Routing to the right backend

    Dynamic configuration selects the upstream, credential, headers, paths and plugins per request from identity and request context, which covers API versioning, tenant separation and staged rollouts on one URL.

    Read more
  5. 05

    Reshaping the request

    Request and response headers can be added, overridden, appended or removed, and path prefixes can be added, removed or replaced, without changing the upstream service.

    Read more
  6. 06

    Protecting the upstream

    Plugins provide sliding-window rate limiting backed by the Cluster's own store, JSON schema validation with a configurable failure response, response caching with a custom key, and direct responses that never reach the upstream at all.

    Read more
  7. 07

    Running your own logic

    Lua scripts and Envoy ExtProc servers run in the request path, before or after authorization, for enrichment, redaction, guardrails and intentional mutation of request and response bodies.

    Read more
  8. 08

    Recording what happened

    Each request produces an identity-aware AccessLog with the method, path, response code, gRPC package, service and method where applicable, streamed in real time over OpenTelemetry to your own log management and SIEM providers.

    Read more

Proxying a request is the easy part. What makes a gateway worth operating is that the identity, the decision, the credential and the record all live in one declarative configuration rather than in four systems.

Questions

Frequently asked

How do applications authenticate?
Through the standard OAuth2 client credentials flow against the Cluster's own token endpoint, or with an access token Credential used directly as a bearer token, sent in an Authorization: Bearer header or an X-Octelium-Auth header. Ordinary OAuth2 libraries in any language work without a proprietary SDK. Workloads can also authenticate secretlessly with federated OIDC assertions from the platform hosting them.
What can a policy actually see?
For HTTP, the method, path, headers, query parameters and the serialized JSON body through ctx.request.http.bodyMap. For gRPC, the package, service, method and full service name, alongside the underlying HTTP request. Those combine with identity, group membership, device posture, session type and time in the same CEL or Open Policy Agent expression, evaluated per request.
Does it do rate limiting and caching?
Yes, as plugins on the Service. Rate limiting is a global sliding window backed by the Cluster's Redis store, applied per Session by default with a custom key available through a CEL expression, and several limits can run together. Response caching takes a TTL and its own key expression. JSON schema validation and direct responses are plugins as well.
Can one URL serve several API versions or accounts?
Yes. Dynamic configuration selects the upstream, upstream credential, headers, paths and plugins per request from identity and request context, so a beta group can be routed to a newer backend with its own key while everyone else stays on the current one, under one Service and one set of Policies.
Does it handle gRPC?
Yes. The gRPC mode automatically listens over HTTP/2 as the protocol requires, and decodes requests so that the package, service and method are available for authorization and appear in the access logs alongside the underlying HTTP information.
Can it host the API as well as front it?
Yes. Managed containers deploy, scale and serve a container image as the Service upstream, on the Cluster's own data-plane nodes, with environment variables from Secrets, replica counts, resource limits, volumes and health probes. A Service can also load balance across several upstream endpoints.
Get started

Run Octelium on your own infrastructure

Install a self-hosted Cluster and start applying identity-aware access control to your Services.