Octelium documentation · Latest

First Steps to Managing the Cluster

This guide walks through the main capabilities of an Octelium Cluster from a management perspective. It starts with a minimal Service and gradually introduces the different Service modes, secretless access, dynamic configuration and public access, then moves on to managing human and workload Users, including AI agents, and finally to controlling access with application-layer aware Policies. Every section links to the detailed management docs and guides where you can learn more.

An Octelium Cluster is managed similarly to a Kubernetes cluster. You describe your resources (e.g. Services, Users, Groups and Policies) declaratively in YAML files, and a single octeliumctl apply command synchronizes the Cluster state with them (read more here). Your entire Cluster configuration can therefore be stored in a Git repository where every change is reviewed, tracked and rolled back like any other code.

note

You can use Octelium Agent Skills with AI coding agents (e.g. Claude Code, Codex, etc...) to help you manage your Cluster. For example, you can ask your agent to create your first Users, Services, Groups and Policies, and it can prepare the YAML configuration and run the relevant octeliumctl commands for you. You can find the installation instructions here or via Ask AI → AI agent skills above.

Before You Begin

You need a running Cluster. If you do not have one yet, the quick installation guide here installs a single-node Cluster on any cheap cloud VM/VPS in a few minutes. You also need the Octelium CLIs on your own machine: octeliumctl manages the Cluster resources, while octelium connects to the Cluster and accesses its Services (read more here).

Since a Cluster is addressed by its domain, the commands and configurations in this guide use a placeholder for it. You can enter your own domain below to use it in all the examples:

Optional. Enter your Cluster domain to use it in place of <DOMAIN> in all the commands and configurations on this page. It is only stored in your browser.

Now install the CLIs and log in to the Cluster. Since the CLIs can work with multiple Clusters, it is easier to set the OCTELIUM_DOMAIN environment variable once instead of adding the --domain flag to every command:

curl -fsSL https://octelium.com/install.sh | bash export OCTELIUM_DOMAIN=<DOMAIN> # Use the authentication token shown at the end of the Cluster installation octelium login --auth-token <AUTHENTICATION_TOKEN>
note

The initial authentication token belongs to the root User that is created during the installation, and the Session it creates is allowed to access everything. Once you have defined your own User and an IdentityProvider as shown below, you can log in via your browser simply using octelium login and delete the initial Credential via octeliumctl delete cred root-init (read more about logging in here).

Applying Changes

Throughout this guide, we add resources to YAML files in a configuration directory (e.g. ~/octelium) and apply them all at once. You are free to split your resources across as many files and subdirectories as you want (read more here):

# Create or update all the resources defined in the directory octeliumctl apply ~/octelium # Show the changes without applying them, including the resources that # would be deleted since they no longer exist in the directory octeliumctl apply --dry-run --prune ~/octelium # Validate the resources locally without connecting to the Cluster (e.g. in CI) octeliumctl validate ~/octelium # List and inspect resources octeliumctl get service octeliumctl get service first-service -o yaml

Here are the main resources that you will manage in this guide:

ResourceRepresents
ServiceA protected resource (e.g. an API, a web app, an SSH server, a database, an LLM provider, an MCP server) implemented by an identity-aware proxy
NamespaceA group of Services that also acts as their parent domain
UserA HUMAN or WORKLOAD identity
GroupA set of Users that share the same Policies
PolicyA set of access control rules written in CEL or OPA
IdentityProviderA GitHub, OpenID Connect or SAML 2.0 provider for web login, or an OIDC issuer trusted for workload assertions
CredentialAn authentication token, OAuth2 client credential or access token issued for a User
SecretA sensitive value (e.g. an upstream API key or password) referenced by other resources by its name
SessionThe short-lived state through which an authenticated User interacts with the Cluster
note

If you're familiar with Kubernetes, you might have noticed the resemblance between Octelium resources and Kubernetes resources. However, Octelium resources are neither Kubernetes resources nor CRDs. Octelium has its own gRPC-based API Server and its resources are stored in its PostgreSQL-based data store via the Resource Server. The same APIs are also available programmatically via the SDKs (read more here).

Services

Each protected resource is represented by a Service, which is implemented by an identity-aware proxy called Vigil. Every request to a Service is authenticated and authorized on a per-request basis before it is proxied to the upstream, and the upstream itself does not need to be aware of Octelium at all (read more about how Octelium works here).

Your First Service

We start with the simplest possible HTTP Service, named first-service, whose upstream is a public website. Add it to a file in your configuration directory (e.g. ~/octelium/services.yaml) as follows:

kind: Service metadata: name: first-service spec: mode: HTTP config: upstream: url: https://example.com

And apply it as follows:

octeliumctl apply ~/octelium

Now connect to the Cluster. The octelium connect command connects your machine to the Cluster over WireGuard/QUIC tunnels and sets up a private DNS for its Services (read more here):

# Connect in the background via the detached mode octelium connect -d # OR in the foreground sudo -E octelium connect

Once connected, you can access the Service by its name using any HTTP client:

curl first-service # This is equivalent to curl first-service.local.<DOMAIN>
note

octelium connect adds local.<DOMAIN>, which is the common private suffix of all the Cluster's Services, to your machine's DNS search domains, which is why the hostname first-service simply works. Every Service is also assigned stable private dual-stack addresses regardless of the addresses of its upstream (read more about Service DNS here).

You can also run octelium connect as an unprivileged process and map the Service to a localhost port instead (read more here):

octelium connect -p first-service:8080 # From another terminal curl localhost:8080

Upstreams

The upstream does not have to be reachable from the Cluster itself. A Service can serve any of the following types of upstreams (read more here):

An upstream that is directly reachable from within the Cluster, such as a Kubernetes service in the same Kubernetes cluster, a resource in the same private cloud network, or a public SaaS API:

kind: Service metadata: name: first-service spec: mode: HTTP config: upstream: url: http://my-api.default.svc:8080

Modes and Secretless Access

Besides HTTP, Vigil understands several application-layer protocols. This application-layer awareness enables Policies to control access based on protocol-specific information (e.g. HTTP paths, SQL queries, LLM models, MCP tools), enables the access logs to record such information, and enables the Service to authenticate to the upstream on behalf of the User. The latter is called secretless access: the upstream credential (e.g. an API key, an SSH private key, a database password) is stored in the Cluster as a Secret and injected by the Service on-the-fly, so it is never shared with, distributed to or stored by any User (read more here).

Secrets are created via the octeliumctl create secret command, which reads the value from a prompt or a file, so that sensitive values are never stored along with your YAML configuration (read more here). Here are some examples:

The HTTP mode supports bearer, basic, custom header, OAuth2 client credentials and AWS SigV4 authentication to the upstream (read more here). Here is a Service for the GitHub API:

kind: Service metadata: name: github-api spec: mode: HTTP config: upstream: url: https://api.github.com http: auth: bearer: fromSecret: github-token
octeliumctl create secret github-token octeliumctl apply ~/octelium # No GitHub token is needed on the client side curl github-api/user

You can see more detailed examples for SaaS APIs, AWS S3 and AWS Lambda.

note

Octelium also supports the KUBERNETES mode with secretless access to Kubernetes API servers, the GRPC, WEB and DNS modes, as well as the generic TCP and UDP modes. Services can also authenticate to mTLS-protected upstreams via client certificates stored as Secrets (read more here).

Embedded SSH and SOCKS5

The octelium client can itself serve SSH and SOCKS5 from the host it runs on, without an SSH server or a SOCKS5 proxy running on that host. Embedded SSH can even be served by an unprivileged octelium process. This is useful for fleets of servers, containers and IoT devices. The Cluster already includes the built-in essh.octelium and socks5.octelium Services for this purpose, so there is nothing to configure (read more about embedded SSH here and embedded SOCKS5 here):

# On the host to be served octelium connect --essh --esocks5 # From any other connected machine, find the name of the serving Session octeliumctl get sess # SSH into its host by using the Session name as the SSH user ssh usr1-9o2ztp@essh.octelium # OR via the SSH client built into the octelium CLI octelium ssh usr1-9o2ztp # Use its host as a SOCKS5 exit node curl --socks5 usr1-9o2ztp@socks5.octelium:1080 https://checkip.amazonaws.com

You can also copy files from and to such hosts via the octelium cp command (read more here).

Dynamic Configuration

So far, all the requests to a Service use the same configuration. Dynamic configuration enables the Service to choose a different upstream, a different upstream account and credential, or different protocol-specific settings on a per-request basis depending on the identity and context of the request. Each rule has a condition that is written exactly like a Policy rule condition, the first matching rule chooses the named configuration, and the default config is used whenever no rule matches. A named configuration can also inherit from another one via parent (read more here):

Members of the ops Group log in as root, while every other authorized User logs in as the unprivileged ubuntu user:

kind: Service metadata: name: prod-server spec: mode: SSH config: upstream: url: ssh://10.0.1.15 ssh: user: ubuntu auth: privateKey: fromSecret: ssh-private-key upstreamHostKey: key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... dynamicConfig: configs: - name: root parent: default ssh: user: root auth: privateKey: fromSecret: ssh-root-private-key rules: - condition: match: '"ops" in ctx.user.spec.groups' configName: root
note

Dynamic configuration is also supported by the other modes, including MYSQL, RDP, SOCKS5, LLM, MCP, DNS, TCP and UDP. Instead of choosing a named configuration, a rule can also generate the configuration on-the-fly via a CEL or OPA expression (read more here).

HTTP-Specific Features

Services that use the HTTP-based modes (i.e. HTTP, WEB, GRPC, LLM and MCP) also provide the capabilities of an API gateway such as header and path manipulation, request body validation, retries and CORS (read more here). Moreover, plugins are invoked conditionally on a per-request basis to provide rate limiting, caching, JSON schema validation, direct responses, Lua scripts and Envoy ExtProc (read more here). Here is an example:

kind: Service metadata: name: api spec: mode: HTTP config: upstream: url: http://api-v2.internal:8080 http: header: # Pass the User's identity to the upstream addRequestHeaders: - key: X-User-Email eval: ctx.user.spec.email removeResponseHeaders: - Server plugins: - name: per-user-rate-limit condition: matchAny: true rateLimit: limit: 100 window: minutes: 1 key: perUser: true - name: cache-assets condition: match: ctx.request.http.path.startsWith("/assets/") cache: ttl: hours: 1 - name: hide-internal-paths condition: match: ctx.request.http.path.startsWith("/internal/") direct: statusCode: 404

LLM and MCP Gateways

The LLM and MCP modes are HTTP-based modes that understand the LLM inference APIs (i.e. OpenAI, Anthropic, Gemini and Bedrock) and the Model Context Protocol respectively. In addition to the HTTP capabilities above, they validate the requests, expose normalized protocol information (e.g. the LLM model and operation, or the MCP method and tool name) to Policies and dynamic configuration, provide protocol-aware plugins and emit protocol-aware access logs (read more about the LLM mode here and the MCP mode here):

An OpenAI gateway that is exposed publicly (see public access below) so that applications and AI agents can use it without the octelium client:

kind: Service metadata: name: llm spec: mode: LLM isPublic: true config: upstream: url: https://api.openai.com llm: protocol: OPENAI auth: bearer: fromSecret: openai-api-key

Any OpenAI-compatible client or SDK can now use https://llm.<DOMAIN>/v1 as its base URL and an Octelium access token (see workload Users below) instead of the OpenAI API key:

curl https://llm.<DOMAIN>/v1/chat/completions \ -H "Authorization: Bearer $OCTELIUM_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-5-mini", "messages": [{"role": "user", "content": "Hello"}]}'

The upstream can also speak a different protocol than the one served to the clients. For example, OpenAI SDKs can use an Anthropic upstream via protocol translation (read more here).

You can see detailed examples of using Octelium as an AI gateway here and as an MCP gateway here.

Public and Anonymous Access

So far, Services are only accessible privately via octelium connect. HTTP-based Services can also be exposed publicly via the clientless BeyondCorp mode, where HUMAN Users access them via their browsers after logging in via the Cluster's web Portal, and WORKLOAD Users access them via standard OAuth2 client credentials or bearer access tokens without any client or SDK (read more here). Public Services are still authenticated and authorized on a per-request basis. You can also expose a Service for anonymous access, effectively using the Cluster as a hosting platform (read more here):

kind: Service metadata: name: grafana spec: mode: WEB isPublic: true config: upstream: url: http://grafana.monitoring.svc:3000

Authorized Users can now open https://grafana.<DOMAIN> in their browsers, including from phones and tablets. The WEB mode is the HTTP mode for web applications, for which the web Portal additionally shows a "Visit" button.

note

Public Services in the default Namespace are covered by the *.<DOMAIN> wildcard DNS entry and TLS certificate that you set up during the installation. Read more about public DNS here and TLS certificates here.

Namespaces

Every Service belongs to a Namespace, which groups Services by environment, team or project and acts as their parent domain. A Service that does not explicitly specify a Namespace belongs to the default Namespace (read more here). Here is a Namespace and a Service that belongs to it:

kind: Namespace metadata: name: production spec: {} --- kind: Service metadata: name: api.production spec: mode: HTTP config: upstream: url: http://api.production.svc:8080

The Service is privately addressed as api.production (i.e. api.production.local.<DOMAIN>), and as api.production.<DOMAIN> if it is exposed publicly, which requires its own public DNS entry and TLS certificate (read more here). Policies attached to a Namespace apply to all of its Services as shown below.

Users and Identity

A User represents an identity that interacts with the Cluster and accesses its Services. There are two types of Users: HUMAN Users, and WORKLOAD Users for non-human entities such as servers, containers, CI/CD pipelines, applications and AI agents (read more here). Both types share the same identity model: they belong to Groups, are subjected to the same Policies, access the same Services via the same client-based and clientless modes, interact with the Cluster only through short-lived Sessions created upon a successful authentication (read more here), and are identified in the same access logs. They mainly differ in the way they authenticate:

Authentication methodHUMANWORKLOAD
Web login via GitHub, OpenID Connect and SAML 2.0 IdentityProviders✓
Passkey login and MFA via FIDO2/WebAuthn, TOTP and TPM 2.0 Authenticators✓
Authentication token Credentials✓✓
OAuth2 client credentials✓
Access token Credentials used as bearer tokens✓
Secretless OIDC assertions (e.g. GitHub Actions, Kubernetes, Azure)✓

Human Users

HUMAN Users log in via the Cluster's web Portal using your identity providers. First, create an OAuth2/OIDC or a SAML 2.0 application in your identity provider and set its callback URL to https://<DOMAIN>/callback. For GitHub and OpenID Connect providers, store the application's client secret as a Secret:

octeliumctl create secret idp-client-secret

Now define the IdentityProvider:

Create an OAuth app in your GitHub developer settings and set its homepage URL to https://<DOMAIN> (read more here):

kind: IdentityProvider metadata: name: github spec: displayName: Login with GitHub github: clientID: Ov23liExampleClientID clientSecret: fromSecret: idp-client-secret

Now define the User and add it to a Group (read more about Groups here). By default, the User's email is matched against the identity returned by any IdentityProvider. You can also bind explicit identities such as a GitHub username (read more here):

kind: Group metadata: name: dev spec: {} --- kind: User metadata: name: linus spec: type: HUMAN email: linus@example.com groups: ["dev"] authentication: identities: - identityProvider: github identifier: torvalds

Once applied, linus can log in via the browser as follows:

octelium login

HUMAN Users can also register FIDO2/WebAuthn (e.g. YubiKeys and platform passkeys), TOTP and TPM 2.0 Authenticators for MFA, re-authentication and passwordless passkey login (read more here). IdentityProviders can also set the authenticator assurance level (AAL) of every login from the provider's assertion and reject logins via post-authentication rules (read more here), which enables Policies to require, for example, a phishing-resistant AAL3 login to access production Services.

Workload Users

WORKLOAD Users are defined in the same way:

kind: User metadata: name: billing-service spec: type: WORKLOAD

A WORKLOAD User can authenticate via any of the following methods, all of which result in a Session that is subjected to the same Policies:

Authentication tokens are used by the octelium and octeliumctl CLIs, typically from servers, containers and Kubernetes pods, where the workload can authenticate and connect in a single step (read more here):

octeliumctl create cred --user billing-service --expire-in 30days billing-auth-token # On the workload side (e.g. inside a container) export OCTELIUM_DOMAIN=<DOMAIN> export OCTELIUM_AUTH_TOKEN=<AUTHENTICATION_TOKEN> octelium connect

You can read more about connecting from containers and Kubernetes.

AI Agents

AI agents are managed as WORKLOAD Users. They therefore share the same identity, authentication, access control and auditing model as your HUMAN Users and other workloads instead of requiring a separate system:

  • Identity without stored secrets An agent can authenticate via an OIDC assertion of the environment it runs in (e.g. its Kubernetes service account token or its GitHub Actions identity), as well as via OAuth2 client credentials, access tokens or authentication tokens.

  • Secretless access to models, tools and infrastructure The agent accesses LLM providers, MCP servers, SaaS APIs, databases and SSH servers via Services that inject the upstream credentials. Since the agent never holds a provider API key, a database password or an SSH private key, a manipulated agent (e.g. via prompt injection) cannot leak them, and revoking the agent's access is a matter of changing a Policy or disabling its User instead of rotating credentials.

  • Per-request, application-layer aware control Policies can restrict every agent to certain LLM models and operations, MCP tools and tool arguments, HTTP paths or SQL statements, while LLM plugins can enforce guardrails and token budgets per agent.

  • Attribution Every LLM request, MCP tool call and database query is logged along with the agent's User and Session.

Here is an example of an agent running as a pod in an EKS cluster that authenticates via its Kubernetes service account token:

kind: IdentityProvider metadata: name: eks-cluster spec: oidcIdentityToken: issuerURL: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE audience: https://<DOMAIN> --- kind: Group metadata: name: ai-agents spec: {} --- kind: User metadata: name: support-agent spec: type: WORKLOAD groups: ["ai-agents"] authentication: identities: - identityProvider: eks-cluster identifier: system:serviceaccount:agents:support-agent

The agent exchanges its service account token, projected with the https://<DOMAIN> audience, for an Octelium access token via the standard OAuth2 JWT assertion flow, and then uses the access token as the API key of its OpenAI and MCP clients for the llm and tickets-mcp Services shown above:

curl -X POST \ -d grant_type=client_credentials \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ -d client_assertion=$(cat /var/run/secrets/tokens/octelium) \ https://<DOMAIN>/oauth2/token

For agents that also need an isolated environment to run in, Cordium is an open source, self-hosted sandbox platform built on Octelium. Every Cordium Workspace has its own Octelium Session identity, so coding agents (e.g. Claude Code and Codex) and automated jobs running inside it access the authorized Services with standard tools such as curl, psql and ssh, while the upstream credentials never enter the sandbox (read more here).

Access Control

Octelium follows the zero trust security model: being authenticated or connected to the Cluster does not grant access to anything. Every request is denied unless a Policy explicitly allows it, and there is no built-in "admin" or "superuser" User (read more here). A Policy is a set of rules, each of which has an ALLOW or DENY effect and a condition written in CEL or OPA. For every request, Octovigil evaluates the Policies attached to the Session, the User, the User's Groups, the Service and its Namespace, as well as the global Policies. A matching DENY rule denies the request, otherwise a matching ALLOW rule allows it, otherwise the request is denied (read more here).

For every request, Octovigil, the policy decision point, collects the rules of every Policy that applies to it: those attached to its Session, its User and the User's Groups, its Service and the Service's Namespace, and the Cluster's global Policies. It evaluates every DENY rule first; if any matches, the request is denied, whatever the ALLOW rules say. Only if none matches are the ALLOW rules evaluated: if one matches the request is allowed, and if none does it is denied by default. Four requests show each case: an ALLOW rule matching, a DENY rule matching, no rule matching, and a DENY rule overriding an ALLOW rule that would have matched.

Granting Access

The simplest way to grant a User full access is to attach the allow-all Policy, which is created during the installation. This is typically how you define your own administrator User instead of using the initial root User:

kind: User metadata: name: alice spec: type: HUMAN email: alice@example.com authorization: policies: ["allow-all"]

For least privilege, you should instead grant access only where it is needed. Rules can be defined inline in any resource, or in standalone Policies that can be reused by any number of resources:

An inline Policy in a Service acts as a resource-based policy. Here, every User whose email belongs to example.com can access first-service:

kind: Service metadata: name: first-service spec: mode: HTTP config: upstream: url: https://example.com authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: match: ctx.user.spec.email.endsWith("@example.com")

Application-Layer Aware Policies

Since Vigil understands the protocol of every request, rules can use the request's content in order to grant access to certain operations instead of the entire resource (read more here):

The members of the dev Group can send GET requests to paths under /api/v1/, while the members of the finance Group can issue refunds of up to 500 according to the JSON request body (read more here):

kind: Service metadata: name: payments-api spec: mode: HTTP config: upstream: url: http://payments.internal:8080 http: enableRequestBuffering: true body: mode: JSON authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: all: of: - match: '"dev" in ctx.user.spec.groups' - match: ctx.request.http.method == "GET" - match: ctx.request.http.path.startsWith("/api/v1/") - effect: ALLOW condition: all: of: - match: '"finance" in ctx.user.spec.groups' - match: ctx.request.http.method == "POST" - match: ctx.request.http.path == "/api/v1/refunds" - match: double(ctx.request.http.bodyMap.amount) <= 500.0
note

Application-layer aware rules are also available for the SSH (e.g. the SSH user), Kubernetes (e.g. verbs, resources and namespaces), gRPC (e.g. services and methods), MySQL, DNS and SOCKS5 (e.g. the requested destination) modes.

Policies also support rule priorities that enable ALLOW rules to override DENY rules, enforcement rules, nested conditions, OPA Rego scripts and global Policies defined in the ClusterConfig (read more here). You can also attach Policies to Credentials in order to restrict the Sessions created by them (read more here). The CEL guide here shows you how to write more advanced conditions.

Visibility

Every request, whether allowed or denied, is recorded in an access log that identifies the User, Session, Device and Service, the Policy that decided the request, and the application-layer details of the request such as HTTP paths, SQL queries, SSH session recordings, LLM models and token usage, or MCP tool calls. Access logs are exported in real time to your OpenTelemetry OTLP receivers, from which they can be sent to your log management and SIEM tools (read more here).

What Now?