Octelium is a free and open source, self-hosted zero trust access platform. Client-based access runs over QUIC or WireGuard tunnels and behaves as a zero-config VPN: a single stable route, stable private dual-stack addresses and automatic private DNS. What makes it zero trust is what sits above the tunnel, where every request is authorized on its own.
Unified identity management for humans, workloads and AI agents
Adaptive authentication for humans and non-human identities
Identity-based, application-layer-aware access control
OpenTelemetry-native real-time visibility and auditing
Unified access architecture for private and SaaS resources
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.
The platform
A unified secure access platform
The tunnel is one of them. Each capability is described in detail below, in the same order.
Each protected resource is represented as a Service behind an identity-aware proxy that enforces application-layer access control, credential isolation and auditing. Connecting to the Cluster establishes a path, and grants nothing by itself.
A client running on this device is asking to log in to acme.com. Approve it only if you have just started a login yourself.
Approve
Reject
Sign in with a passkey
acme.com
J
jane@acme.com
Passkey
Touch the sensor to continueVerifying…Verified
Cancel
~
~❯
New Tab
Search or type a URL
grafana
argocd
jenkins
kibana
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)
Access control
Layer-7 aware access control with policy-as-code
Authorize every request from authenticated identity, request context and application-layer attributes with policy-as-code using CEL or Open Policy Agent.
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"]
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
Application-layer credentials are stored in the Cluster as Secrets and injected into the upstream connection once a request is authorized. A tunnel on its own does nothing about the API keys, database passwords and private keys that reach a resource once the tunnel is up.
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
Identity
One identity model for humans, workloads and AI agents
Human and workload Users share the same identity management, authentication, access control and visibility model. A container in CI connects the same way an engineer does, and is authorized by the same Policies.
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.
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.
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.
What a remote access VPN leaves open, and what closes it
A tunnel solves reachability. These are the problems that remain once the tunnel is up, next to the mechanism in an Octelium Cluster that handles each one.
01
Route tables that grow with the network
Services are addressed through the Cluster rather than through the networks their upstreams sit on, so a client keeps a single stable route no matter how many environments, clouds and clusters are behind it. There are no remote subnets to advertise and no overlapping ranges to reconcile.
Because only Cluster traffic uses the tunnel, ordinary internet traffic never has to be pulled through a gateway to keep a connection usable, which is the usual reason full tunneling gets switched on and the usual source of its egress cost.
Clients connect with a private IPv6 address by default and can be forced to IPv4-only or dual-stack, independently of what each upstream speaks. No NAT64 or DNS64 has to be operated to bridge the two.
Every Service has a stable private FQDN assigned by the Cluster and resolved through an automatically configured split DNS, so dynamic upstreams such as containers, Kubernetes services and cloud-assigned endpoints do not need a DNS zone maintained by hand.
Authorization is expressed against Services, identities and requests rather than IP addresses and subnets, so a new database created inside an allowed subnet is not automatically reachable, and a reused address does not silently inherit an old permission.
Traffic terminates at an identity-aware proxy that understands the protocol in front of each upstream, so the upstream is not directly reachable by whoever is on the network, including an attacker already inside the perimeter.
Human Users re-authenticate through your identity provider and workload Users through short-lived assertions or OAuth2 credentials, and application-layer credentials for upstreams are held by the Cluster rather than copied to laptops and CI systems.
Every request produces an identity-aware AccessLog with the application-layer detail of the request itself, streamed over OpenTelemetry, instead of connection records that show an address reached a port.
Encrypting the path between two networks is a transport problem and a solved one. What is left is deciding, per request, who may reach which resource and with which credential, and being able to show afterwards what happened.
On every Octelium Cluster
The same platform capabilities ship with every deployment, whichever way you use it.
Both are supported. Octelium uses WireGuard for tunneling by default, and a Cluster can additionally enable a QUIC-based tunneling mode. When the Cluster has it enabled, a client selects it with octelium connect --tunnel-mode quicv0 or by setting the OCTELIUM_QUIC environment variable. Everything above the transport, including identity, Policies, secretless access and auditing, is identical either way.
How is the QUIC mode enabled on a Cluster?
The quick installer takes a --quicv0 flag, and existing Clusters enable it through the pre-installation configuration. The QUIC listener uses UDP port 8443, so that port needs to be open if the machine sits behind a firewall, and an additional public DNS entry is required. WireGuard remains available alongside it.
Do clients need root privileges?
Not necessarily. On Linux the client first tries the WireGuard kernel implementation, which is the most performant and needs root or the NET_ADMIN capability. Without those it falls back to a wireguard-go TUN device, and without permission to create a TUN device it runs an unprivileged userspace network stack over gVisor Netstack. In that last mode Services are reached by mapping them to the host, which is what makes the client usable inside containers, CI runners and confined environments.
What happens to DNS?
Each Service has a private FQDN in the form service.namespace.local.yourdomain, with a shorter form for the default Namespace. The client configures a split DNS server for that suffix automatically, using resolvectl on Linux and networksetup on macOS. Nothing has to be memorized, distributed or kept in sync as upstream addresses change.
Can resources behind NAT be reached without opening a port?
Yes. A Service upstream can be an address that is only reachable from the side of a connected client, which serves it to the Cluster over its own tunnel using the --serve or --serve-all flags. That covers a laptop behind NAT, containers, Kubernetes pods in other clusters, private clouds and IoT devices, with no inbound port and no firewall change on that side.
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.