Fortinet alternative

The open source, self-hosted alternative to Fortinet ZTNA

Octelium is a free and open source, self-hosted zero trust access platform. It provides identity-based, layer-7 aware access control on a per-request basis, dynamic secretless access, and a unified architecture for both client-based and clientless access, on infrastructure you already operate rather than on an appliance.

  • Free and open source
  • No appliance or per-agent licensing

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.
Secretless access

Keep upstream credentials out of users and workloads

Store application credentials in the Cluster and inject them only after access is authorized. Users, workloads and AI agents reach protected resources without receiving the upstream API key, password, private key, certificate or other credential.

Read about secretless access

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 Fortinet ZTNA, 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

This is a curated subset of the full capability matrix, including areas where either product differs and areas where their capabilities are equivalent. Open the complete matrix for every row, source and filter.

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.
Fortinet FortiClient ZTNA
FortiGate appliance/gateway plus FortiClient EMS posture management and FortiClient endpoint agent. Hardware/software security fabric architecture.

Authentication

OpenID Connect

Octelium
Yes
Fortinet FortiClient ZTNA
Yes

SAML 2.0

Octelium
Yes
Fortinet FortiClient ZTNA
Yes

Native FIDO2 / Passkey

Native means implemented by the platform itself, not merely delegated to an external IdP.

Octelium
NativeFIDO2/Passkey/WebAuthnN with attestation and hardware-resistant authenticator checks
Fortinet FortiClient ZTNA
No

Workload Identity

Octelium
OIDC assertions / workload identity
Fortinet FortiClient ZTNA
No

SCIM Provisioning

Automated user/group lifecycle provisioning via SCIM, beyond just login-time IdP federation.

Octelium
SCIM 2.0 compliant
Fortinet FortiClient ZTNA
FortiAuthenticator / EMS; SCIM support varies by component

Authorization & Policy

Policy Model

Octelium
Layer-7 aware ABAC with policy-as-code via CEL and OPA
Fortinet FortiClient ZTNA
Fortinet tags, groups, EMS posture, firewall/security fabric policy, and RBAC-style administration.

Policy-as-Code

Octelium
CEL + OPA
Fortinet FortiClient ZTNA
No

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
Fortinet FortiClient ZTNA
NoPrimarily session/network/security-policy oriented.

L7-Aware Policies

Octelium
HTTP, gRPC, K8s, DB, SSH, mTLS-related context
Fortinet FortiClient ZTNA
Partial

Zero Standing Privilege / JIT

Indicates strong architectural support for minimizing standing privilege; not an absolute claim that privileged access cannot exist.

Octelium
StrongDesigned to minimize static privilege through identity/policy-scoped access.
Fortinet FortiClient ZTNA
No

Access Requests / Approvals

First-class request-and-approve / break-glass workflows (reviewers, time-bound grants), as opposed to static policy alone.

Octelium
Access requests + approvalsFirst-class access request and approval workflow, in addition to policy/identity-scoped access that can be time-bound.
Fortinet FortiClient ZTNA
No

Device Posture

Octelium
TPM/FIDO2/device attributes
Fortinet FortiClient ZTNA
FortiClient EMS

L7 Protocol Awareness & Secretless Access

HTTP / HTTPS

Octelium
L7-aware + secretless credential injection
Fortinet FortiClient ZTNA
Partial

SSH

Octelium
Secretless / embedded SSH patterns
Fortinet FortiClient ZTNA
No

PostgreSQL

Octelium
Secretless + query-aware policy/logging
Fortinet FortiClient ZTNA
No

Kubernetes

Octelium
Secretless + verb/resource/namespace policy
Fortinet FortiClient ZTNA
No

RDP

Octelium
with clientless secretless browser access
Fortinet FortiClient ZTNA
Partial

DNS

Octelium
Native private DNS mode
Fortinet FortiClient ZTNA
FortiGuard DNS/security features

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.
Fortinet FortiClient ZTNA
NoIPsec / SSL VPN and ZTNA agent transport, not WireGuard.

IPv6 Support

Octelium
Dual-stackDefaults to IPv6, with selectable IPv4-only or dual-stack cluster network ranges.
Fortinet FortiClient ZTNA
Enterprise IPv6

NAT Traversal

Octelium
Via GatewaysReaches resources behind NAT through Cluster Gateways; client-to-Gateway path rather than a direct P2P mesh.
Fortinet FortiClient ZTNA
Partial

Visibility, Auditing & Observability

OpenTelemetry-Native

Octelium
Yes
Fortinet FortiClient ZTNA
No

L7-Aware Access Logs

Octelium
HTTP/gRPC/K8s/DB/SSH identity-aware logs
Fortinet FortiClient ZTNA
Partial

SSH Session Recording

Octelium
Yes
Fortinet FortiClient ZTNA
No

SIEM Integration

Octelium
Via OTLP / OTel Collector
Fortinet FortiClient ZTNA
FortiAnalyzer/FortiSIEM

Access Methods

Client-Based VPN / Overlay

Octelium
WireGuard / QUIC
Fortinet FortiClient ZTNA
FortiClient / SSL VPN / ZTNA agent

Clientless Browser Access

Octelium
Yes
Fortinet FortiClient ZTNA
No

Private DNS

Octelium
Yes
Fortinet FortiClient ZTNA
No

Dynamic Configuration

GitOps / Declarative Config

Octelium
Kubernetes-like resources / YAML
Fortinet FortiClient ZTNA
No

AI / MCP / Agent Compatibility

AI / LLM Gateway

Octelium
Native LLM Service modeProtocol-aware OpenAI/Anthropic gateway: validated operations, per-request authorization on model, tools and token limits, CEL-based model selection, and token usage in access logs.
Fortinet FortiClient ZTNA
No

MCP Gateway / MCP Access

Octelium
Native MCP Service modeProtocol-aware MCP gateway: JSON-RPC method and tool-level authorization, protocol version pinning, origin validation, and managed-container MCP upstreams.
Fortinet FortiClient ZTNA
No

Deploy Containers / PaaS

Octelium
Yes
Fortinet FortiClient ZTNA
No

Scalability & Cloud-Nativeness

Kubernetes-Native

Octelium
Built on Kubernetes
Fortinet FortiClient ZTNA
No

High Availability

Octelium
K8s-native HA model
Fortinet FortiClient ZTNA
HA clustering

Openness & Self-Hosting

Fully Self-Hosted

Octelium
Yes
Fortinet FortiClient ZTNA
On-prem appliance, but proprietary ecosystem

Server Code Open Source

Octelium
Yes
Fortinet FortiClient ZTNA
No

No Mandatory Proprietary Cloud

Octelium
Yes
Fortinet FortiClient ZTNA
On-prem possible, but proprietary Fortinet stack

Data Sovereignty

Octelium
Yes
Fortinet FortiClient ZTNA
On-prem deployments
Questions

Frequently asked

Is Octelium free and open source?
Yes. Octelium is free and open source and is designed for single-tenant self-hosting. There is no proprietary cloud-based control plane, no appliance to buy and no per-agent licensing, and it is not a limited edition of a separate paid product. An enterprise package is available for organizations that need capabilities such as a web console, SCIM provisioning and secret encryption at rest, and it is free for personal, homelab and evaluation use.
What hardware does it need?
None in particular. An Octelium Cluster runs on Kubernetes, on hardware or virtual machines you already operate. The quick installer provisions a complete single-node Cluster, including Kubernetes itself, on one fresh Linux VM with 2 vCPUs, 2 GB of RAM and a domain you own. Production Clusters run on scalable managed or on-premise Kubernetes installations, and Gateways scale horizontally rather than each one imposing a fixed ceiling.
Can it enforce device posture?
Yes. Device information including the operating system type is available in the request context and can be combined with identity, group membership, session type and time in the same policy expression. Octelium also provides native FIDO2 and WebAuthn passkeys, TOTP and TPM 2.0 authenticators that can be required through policy on sensitive Services, independently of what the identity provider itself enforces.
Does it replace a firewall?
No. Octelium is a zero trust access platform: it represents each protected resource as a Service behind an identity-aware proxy and authorizes access to it per request. It does not perform network segmentation, intrusion prevention or egress filtering for a whole site, and it is normally deployed alongside whatever network security controls an organization already operates.
Can I keep my existing identity provider?
Yes. Any OpenID Connect or SAML 2.0 identity provider is supported, as well as GitHub OAuth2. Human Users authenticate through it, and workload Users authenticate with authentication tokens, the OAuth2 client credentials flow, or federated OIDC assertions issued by the platform hosting them.
How is the Cluster managed?
Declaratively, in the way a Kubernetes cluster is managed. Resources are defined in YAML, kept in a Git repository, and applied with a single octeliumctl apply that synchronizes the entire Cluster state, which makes changes reviewable and reversible. Everything is also available over the Cluster's gRPC APIs, so no configuration change requires console or SSH access to a device.
Get started

Run Octelium on your own infrastructure

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