Cloudflare Tunnel alternative

The open source, self-hosted alternative to Cloudflare Tunnel

Octelium is a free and open source, self-hosted zero trust access platform. A resource behind NAT is served to the Cluster over an outbound tunnel from a connected client, so it can be reached privately or published to the internet without an inbound port, on infrastructure you operate end to end.

  • Free and open source
  • No inbound ports
  • Runs on your own infrastructure

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
Deployment and scale

Self-hosted, Kubernetes-native and fully programmable

Run the Cluster on Kubernetes infrastructure you control, scale the data plane horizontally, manage resources declaratively, automate the platform through APIs and SDKs, and optionally deploy containerized Service upstreams with the Cluster itself.

Read the management guide

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
Comparison

Octelium and Cloudflare Tunnel, capability by capability

Both products are compared from the same structured capability dataset and evaluated using the same evidentiary standard. Emerging or experimental capabilities are marked as partial regardless of vendor.

Capability comparison

Dataset reviewed August 2026

A curated view of the dataset, weighted here towards tunneling, publishing and hosting. It includes the rows where Octelium is stronger, the rows where it is not, and the rows where the two are equivalent. The complete capability matrix, with search and filters, is one click away.

Architecture

Architecture

Octelium
Kubernetes-native identity-aware proxy/gateway architecture in which policy evaluation is separated from enforcement, with client-based WireGuard/QUIC access, clientless BeyondCorp access, declarative resources, and self-hosted control/data planes.
Cloudflare Access / Tunnel
SaaS edge proxy/SASE architecture. cloudflared connectors and/or WARP clients connect private resources and users to Cloudflare's global network. Access policies apply at Cloudflare-controlled edge/control plane.

Authentication

Anonymous / Public Access

Octelium
Yes
Cloudflare Access / Tunnel
No

Authorization & Policy

Policy-as-Code

Octelium
CEL + OPA
Cloudflare Access / Tunnel
Terraform/API; not a CEL/OPA product-native policy runtime

Per-Request Authz

For L7 products, this means each HTTP/gRPC/API/K8s request can be evaluated. For overlays, session/connection policy is not counted as per-request. This modeling choice structurally favors L7 gateways over network overlays.

Octelium
Yes
Cloudflare Access / Tunnel
For Access-protected HTTP/MCP/web flows; not equivalent for all private network flows

L7-Aware Policies

Octelium
HTTP, gRPC, K8s, DB, SSH, mTLS-related context
Cloudflare Access / Tunnel
Strong HTTP/web policy; not DB/K8s query/verb semantics

L7 Protocol Awareness & Secretless Access

HTTP / HTTPS

Octelium
L7-aware + secretless credential injection
Cloudflare Access / Tunnel
Access / reverse proxy

gRPC

Octelium
Native L7 mode
Cloudflare Access / Tunnel
Transport/web proxy patterns; not general gRPC semantic authz

SSH

Octelium
Secretless / embedded SSH patterns
Cloudflare Access / Tunnel
Browser/CLI access patterns

PostgreSQL

Octelium
Secretless + query-aware policy/logging
Cloudflare Access / Tunnel
Private network/proxy connectivity; not query-aware secretless DB gateway

MySQL

Octelium
Secretless + query-aware policy/logging
Cloudflare Access / Tunnel
Private network/proxy connectivity; not query-aware secretless DB gateway

Kubernetes

Octelium
Secretless + verb/resource/namespace policy
Cloudflare Access / Tunnel
Can protect access paths; not native K8s verb/resource authz gateway

RDP

Octelium
with clientless secretless browser access
Cloudflare Access / Tunnel
Browser-based RDP and client modes

DNS

Octelium
Native private DNS mode
Cloudflare Access / Tunnel
Gateway DNS filtering / private networking DNS patterns

Raw TCP / UDP

Octelium
TCP and UDP
Cloudflare Access / Tunnel
Private network access through WARP/Tunnel

HTTP Manipulation

Octelium
With native plugins including Lua, Envoy ExtProc, JSON schema validation, cache, rate limits
Cloudflare Access / Tunnel
Transform Rules/WAF/Workers depending on product composition

Transport & Networking

WireGuard Data Plane

Whether the product's own client/data plane is built on WireGuard, versus a proprietary or TLS-based transport.

Octelium
Kernel / userspaceWireGuard via kernel module, TUN, or unprivileged userspace (wireguard-go) implementations.
Cloudflare Access / Tunnel
WARPWARP uses a WireGuard-derived transport (BoringTun) alongside MASQUE/HTTP3; not a standard WireGuard mesh.

QUIC Transport

Product-native QUIC transport/tunneling. Experimental or internal-only QUIC is marked partial.

Octelium
Full QUIC-based tunneling mode
Cloudflare Access / Tunnel
QUIC / HTTP3 / MASQUEQUIC and HTTP/3 are first-class across Cloudflare's edge and WARP transport.

IPv6 Support

Octelium
Dual-stackDefaults to IPv6, with selectable IPv4-only or dual-stack cluster network ranges.
Cloudflare Access / Tunnel
Yes

NAT Traversal

Octelium
Via GatewaysReaches resources behind NAT through Cluster Gateways; client-to-Gateway path rather than a direct P2P mesh.
Cloudflare Access / Tunnel
PartialEdge-terminated through Cloudflare's network, not a P2P data path.

Visibility, Auditing & Observability

OpenTelemetry-Native

Octelium
Yes
Cloudflare Access / Tunnel
No

L7-Aware Access Logs

Octelium
HTTP/gRPC/K8s/DB/SSH identity-aware logs
Cloudflare Access / Tunnel
HTTP/access/gateway logs; not DB query logs

SSH Session Recording

Octelium
Yes
Cloudflare Access / Tunnel
No

SIEM Integration

Octelium
Via OTLP / OTel Collector
Cloudflare Access / Tunnel
Logpush / SIEM integrations

Access Methods

Client-Based VPN / Overlay

Octelium
WireGuard / QUIC
Cloudflare Access / Tunnel
WARP client

Clientless Browser Access

Octelium
Yes
Cloudflare Access / Tunnel
Yes

Private DNS

Octelium
Yes
Cloudflare Access / Tunnel
Private DNS/Gateway patterns, not MagicDNS-style product focus

Dynamic Configuration

Dynamic Upstream Routing

Octelium
Yes
Cloudflare Access / Tunnel
Partial

GitOps / Declarative Config

Octelium
Kubernetes-like resources / YAML
Cloudflare Access / Tunnel
Terraform/API

SaaS / Public Cloud API Access

Secretless SaaS API Access

Octelium
Yes
Cloudflare Access / Tunnel
No

AI / MCP / Agent Compatibility

Deploy Containers / PaaS

Octelium
Yes
Cloudflare Access / Tunnel
No

Gateway Capabilities

API Gateway

Octelium
Rate limit, cache, schema validation, Lua, ExtProc
Cloudflare Access / Tunnel
WAF/Rules/Workers/API Shield composition

Kubernetes Ingress / Gateway

Octelium
Yes
Cloudflare Access / Tunnel
No

Reverse Proxy / Tunneling

Octelium
Yes
Cloudflare Access / Tunnel
Yes

Scalability & Cloud-Nativeness

Kubernetes-Native

Octelium
Built on Kubernetes
Cloudflare Access / Tunnel
SaaS edge; deploys connectors/operators but not K8s-native product

High Availability

Octelium
K8s-native HA model
Cloudflare Access / Tunnel
Global edge

Openness & Self-Hosting

Fully Self-Hosted

Octelium
Yes
Cloudflare Access / Tunnel
No

Server Code Open Source

Octelium
Yes
Cloudflare Access / Tunnel
No

No Mandatory Proprietary Cloud

Octelium
Yes
Cloudflare Access / Tunnel
No

Data Sovereignty

Octelium
Yes
Cloudflare Access / Tunnel
NoTraffic/control plane depend on Cloudflare's network for core workflows.
Questions

Frequently asked

How does a resource behind NAT get reached without an inbound port?
A Service upstream can be an address that is only reachable from the side of a connected client. That client, running octelium connect with the --serve or --serve-all flag, announces that it is willing to serve the Service, and the Cluster forwards requests to it over the client's own outbound tunnel. Nothing has to be opened on the network the resource sits on, which covers laptops, containers, Kubernetes pods in other clusters, private clouds and IoT devices.
Where does traffic terminate?
On your own Cluster. Octelium is designed for single-tenant self-hosting: the control plane and the data plane both run on Kubernetes that you operate, on the cloud provider or on-premise infrastructure of your choice. There is no vendor edge in the path, and no third party terminates TLS for your traffic.
Can it host something publicly, not just protect it?
Yes. A Service can be published anonymously, which switches off authentication and authorization for that Service and makes it an ordinary public endpoint, suitable for a website or a public API. Anonymous authorization can then still be enabled, so the Service can filter on path, method, headers, query parameters and body content while remaining publicly reachable.
Is it only for HTTP?
No. Alongside HTTP, WEB and gRPC, a Service can run in SSH, PostgreSQL, MySQL, Kubernetes, DNS, SOCKS5, browser-based RDP, MCP and LLM modes, or in generic TCP and UDP mode for anything else. The application-layer modes add per-request authorization, secretless upstream credentials and protocol-aware auditing on top of the tunnel.
Does the Cluster need to reach the resource at all?
Not necessarily. Besides serving through a connected client, the Cluster can also deploy the application itself: managed containers run a container image as the Service upstream, scheduled on the Cluster's own data plane nodes, with environment variables from Secrets, replica counts, resource limits, volumes and health probes.
Is Octelium free and open source?
Yes. Octelium is designed for self-hosting and does not require a proprietary hosted control plane. The core platform is free and open source. An optional enterprise package adds capabilities for organizations with additional operational, identity-management and security requirements, including the web console, SCIM provisioning and Secret encryption at rest.
Get started

Run Octelium on your own infrastructure

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