First Steps to Managing the Cluster

An Octelium Cluster is designed to be managed similarly to Kubernetes clusters. The main way to manage an Octelium Cluster is via the octeliumctl CLI. If you're accustomed to kubectl, you will feel right at home very quickly as it follows the same declarative philosophy enabling you to define your resources in one or more yaml files and by just using a single command (i.e. octeliumctl apply) you can apply all changes to the Cluster and synchronize its state (read more here). The declarative way enables you to grow your Cluster resources very easily in an organized and trackable way that can be stored in a Git repository where you can effortlessly update/rollback your Cluster state with a single command.

Creating our first Service

Each protected resource is represented in the Cluster by a Service. A Service is implemented by an identity-aware proxy (IaP) called Vigil abstracting all dynamic network-layer details of the protected resource (i.e. upstream) behind it and is capable of providing secure secret-less access that eliminates sharing and managing L7 credentials such as API keys and database passwords (read more about secret-less access here) for various protocols including HTTP, SSH, PostgreSQL, MySQL, among others besides protecting generic TCP/UDP-based applications (read more about Service modes here). The upstream of the Service can be:

  • An internal resource running in any private network (e.g. on-prem, private cloud, your own laptop behind a NAT, etc...) which can be a static IPv4 or IPv6 address or a FQDN with dynamic endpoints as is the case with Kubernetes services for example. Furthermore, you can use an internal resource directly accessible from the private network where the Octelium Cluster (and its underlying Kubernetes cluster) is running.
  • A publicly protected resource such as SaaS APIs, databases and SSH servers. See some examples (here and here).
  • Octelium can also automatically deploy, scale and secure access for your containerized apps and microservices to be served as Services (read more about managed containers here).

We are going to define our first Service with the name first-service whose upstream is the public URL https://www.google.com:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://www.google.com

Serving the public website https://www.google.com as a Service, however, isn't particularly useful, beyond demonstrating that a Service can serve public or internal/private FQDNs that might point to dynamic upstreams with changing IP addresses—a capability remote access VPNs lack since they operate at the network layer/layer-3. We can do something a little more interesting, like deploying the nginx container image and serve it as a Service (read more about managed containers here) as follows:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
container:
9
port: 80
10
image: nginx:latest
11
replicas: 3

A Service can also provide "secret-less" access for the authorized Users to an upstream that requires an application-layer (L7) credential such as HTTP bearer access tokens (read more about secret-less access here). Octelium's application-layer awareness allows you to simply eliminate the need to manage and distribute such credentials, which are often long-lived and over-privileged in many practical cases, at any scale. Here is an example:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.openai.com
9
http:
10
path:
11
addPrefix: /v1
12
auth:
13
bearer:
14
fromSecret: openai-api-key
NOTE

The highlighted openai-api-key is the name of a Secret that actually represents the API access token, referenced in the Service by its name in order to not store sensitive data along with other Cluster configurations which can be stored in git repositories for example (read more about creating Secrets here).

Octelium's application-layer awareness is not exclusive to HTTP-based Services. You can also define an SSH Service that provides secret-less access without having to distribute and share SSH passwords or private keys (read more here). Here is an example:

1
kind: Service
2
metadata:
3
name: ssh1
4
spec:
5
mode: SSH
6
port: 2022
7
config:
8
upstream:
9
url: ssh://address-to-host
10
ssh:
11
user: root
12
auth:
13
password:
14
fromSecret: ssh1-password
15
upstreamHostKey:
16
key: ssh-rsa AAAA...
NOTE

Octelium also supports an "embedded SSH" mode, where you can serve SSH via an embedded SSH server that is running from within the octelium client without having to rely on an existing SSH server on the host. This can be especially useful for constrained environments such as containers and IoT fleets (read more about the embedded SSH mode here).

NOTE

Octelium currently supports several L7 aware modes: SSH, PostgreSQL, MySQL, DNS, gRPC, Web, Kubernetes in addition to the raw TCP and UDP modes.

And you can also serve any remote upstream from the host of any connected User to the Cluster (read more here) as follows:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://www.google.com
9
user: john

This allows you to serve upstreams running behind NAT from anywhere (e.g. private clouds, on-prem, your own laptop, IoT devices, etc...). You can see a more detailed example here.

Now, to actually apply the creation of our Service first-service, we use the octeliumctl apply command as follows:

octeliumctl apply --domain <DOMAIN> /path/to/octelium/config/dir
NOTE

If you're familiar with Kubernetes, you might have noticed the YAML visual resemblance between Octelium resources and Kubernetes resources and now you might be wondering whether Octelium resources are just Kubernetes resources or simply CRDs. The answer is no. Octelium and its resources are completely separate from Kubernetes. Octelium has its own gRPC-based API Server, and its resources are stored in the PostgreSQL data store via the Resource Server.

Since the Octelium CLIs (i.e. octelium and octeliumctl) are designed to work with multiple Clusters as defined by their own domains, you will have to add the --domain flag to every command. It's much easier to define the domain as an environment variable in your shell once and then use any command without having to add the --domain flag as follows:

export OCTELIUM_DOMAIN=<DOMAIN>

Or in Windows PowerShell as follows:

1
$env:OCTELIUM_DOMAIN = "<DOMAIN>"

Now let's connect to our Cluster as follows:

# Connect via the detached mode
octelium connect -d
# OR
sudo -E octelium connect
NOTE

In Windows, run your PowerShell as administrator.

Now that we're connected to the Cluster, we can access the Service first-service simply using any tool that can talk HTTP, let's try curl for example as follows:

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

Why does this hostname first-service work? Simply put, when you connect to a Cluster via the command octelium connect, Octelium automatically configures your machine DNS and adds the suffix .local.<DOMAIN>, which is the common suffix for all Services in the Cluster, to your machine's DNS search domains so you don't have to type the entire private FQDN yourself.

You can also run octelium connect as a completely unprivileged process and map the Service first-service to a localhost port as follows:

octelium connect -p first-service:8080

In the above example, we mapped the Service to the localhost port 8080. Now we can access it as follows:

curl localhost:8080
NOTE

You can read more about connecting to Clusters via the octelium CLI and its more advanced options here.

So far, in order to access our Service first-service, Users will have to use the octelium CLI and connect to the Cluster first. Octelium, however, also supports the client-less BeyondCorp mode, which enables you to securely expose an HTTP-based Service publicly in order to be accessed by authorized HUMAN Users via their browsers and even by WORKLOAD Users through standard OAuth2 client credentials authentication flow (read more here). You can very simply do so as follows:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
container:
10
port: 80
11
image: nginx:latest

And now, authorized Users can publicly access the Service via their browsers at the public URL https://first-service.<DOMAIN>

Octelium can also fully expose a Service for anonymous access. This allows you to effectively use Octelium as a self-hosted PaaS or a hosting platform where you can publicly expose HTTP-based Services to the public internet. Such Services' upstreams might be running anywhere or be deployed as managed containers as we have seen earlier. You can read more about the anonymous access mode here. Here is an example:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: WEB
6
isPublic: true
7
isAnonymous: true
8
config:
9
upstream:
10
container:
11
port: 80
12
image: nginx:latest

You can also use dynamic configuration in order to, for example, route to different upstreams and/or set different upstream credentials mapping to different upstream accounts and permissions, set different request/response headers, etc... depending on the request's identity and/or context (read more about dynamic configuration here). Here is a simple example that can be used for an API gateway use case (read more here):

1
kind: Service
2
metadata:
3
name: my-api
4
spec:
5
mode: HTTP
6
isPublic: true
7
dynamicConfig:
8
configs:
9
- name: v1
10
upstream:
11
url: https://apiv1.example.com
12
http:
13
path:
14
removePrefix: /v1
15
- name: v2
16
upstream:
17
url: https://apiv2.example.com
18
http:
19
path:
20
removePrefix: /v2
21
rules:
22
- condition:
23
match: ctx.request.http.path.startsWith("/v1")
24
configName: v1
25
- condition:
26
match: ctx.request.http.path.startsWith("/v2")
27
configName: v2

Dynamic configuration can also be used for other layer-7 aware modes such as SSH, POSTGRES and MYSQL as well as for TCP and UDP. For example, you can dynamically force certain Users to log in as certain SSH users based on identity or context via policy-as-code. For PostgreSQL or MySQL, you can enforce Users to use certain database users/passwords as well as different databases and even upstreams based on identity and context.

Creating a User

There are 2 types of Users (You can read in detail about User management here): HUMAN Users and WORKLOAD Users which can be used by non-human entities (e.g. servers, VMs and microVMs, containers, applications, IoT devices, etc...).

Both User types can use the private client-based ZTNA mode, which acts as a zero-config VPN from the User's perspective where Users can address Services via stable private FQDNs and hostnames assigned by the Cluster. Both User types can also securely access publicly exposed HTTP-based Services via the client-less, public BeyondCorp mode (read more here) as follows:

  • HUMAN Users can access web-based Services via their browsers without having to install clients on their machines.
  • WORKLOAD Users can use the OAuth2 client credentials flow enabling your applications written in any programming language to access any publicly exposed HTTP-based Service (e.g. HTTP and gRPC APIs, Kubernetes API servers, etc...) solely via standard OAuth2 libraries without having to use any special SDKs or install any clients (read more here). Moreover, Golang-based applications can use the official Golang SDK (read more here) to control the Cluster and access its Services.

We are now going to create our first User for a friend in order to be able to connect to our Cluster and access our Service first-service. As always, we add a new yaml file, in this example users.yaml, in the same directory (i.e. /path/to/octelium/config/dir) dedicated to Users.

Now we can define our User john as follows:

1
kind: User
2
metadata:
3
name: john
4
spec:
5
type: HUMAN

And to actually apply the creation of our User, we use the octelium apply command as follows:

octeliumctl apply /path/to/octelium/config/dir

User Authentication

HUMAN Users can use their emails to authenticate to the Cluster via web browsers using IdentityProviders. There currently 3 methods:

  • GitHub OAuth IdentityProvider as shown in detail here
  • OpenID Connect IdentityProviders (e.g. Okta, Auth0, etc...) as shown here.
  • SAML 2.0 IdentityProviders (e.g. Okta, Entra ID, etc...) as shown here.

For WORKLOAD Users, they can authenticate themselves via the octelium login or octeliumctl login commands using various ways:

  • Authentication token Credentials (read more here)
  • OAuth2 client credentials (read more here)
  • "Secret-less" OpenID Connect identity assertions (read more here).
  • Access tokens directly issued and used as bearer authentication tokens (read more here).

A User can interact with the Cluster and access its Services only through a valid Session that is automatically created by the Cluster upon a valid authentication via an authentication token, OAuth2 client credential authentication, or an IdentityProvider. A User needs to periodically re-authenticate to keep the Session valid until it eventually expires. You can read more about Session management here.

Access Control

Now, while our friend john can actually connect to our Cluster, he still cannot access the Service first-service unless we explicitly allow him via a Policy. For example, we can allow john to access everything in our Cluster by attaching the allow-all Policy as follows:

1
kind: User
2
metadata:
3
name: john
4
spec:
5
type: HUMAN
7
authorization:
8
policies: ["allow-all"]
NOTE

This allow-all Policy is automatically created during the Cluster installation and you can modify/delete it, whenever you want to.

Allowing john to access everything in the Cluster, however, does not sound like a very good idea since we seek to only grant access our first-service Service. For example, we can, instead, define an inline Policy in our Service to allow anybody whose email belongs to our domain example.com to access first-service as follows:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://example.com
9
authorization:
10
inlinePolicies:
11
- spec:
12
rules:
13
- effect: ALLOW
14
condition:
15
match: ctx.user.spec.email.endsWith("@example.com")

As you can see, you are free to either define your access control rules through inline Policies or standalone, reusable Policies that can be attached to any Service, Namespace, User, Group, Device or Session. Here is how to re-define the above inline Policy as a standalone Policy that can be used and attached by other resources:

1
kind: Policy
2
metadata:
3
name: first-policy
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
match: ctx.user.spec.email.endsWith("@example.com")

Now we attach it to our Service as follows:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://example.com
9
authorization:
10
policies: ["first-policy"]

We can also define a Policy for a whole set of Users. This is what a Group is for, among other functionalities (read more about Groups here). Let's define our first Group friends and attach john to it.

1
kind: Group
2
metadata:
3
name: friends
4
spec:
5
authorization:
6
policies: ["first-policy"]

Now let us add friends as one of john's Groups as follows:

1
kind: User
2
metadata:
3
name: john
4
spec:
5
type: HUMAN
6
groups: ["friends"]

Now, any User belonging to the friends Group will be automatically subjected to all of its Policies and inline Policies' rules.

Octelium enables you to seamlessly define dynamic, fine-grained L7-aware Policies. For example, you can control access based on the HTTP request path, method, query parameters, and even serialized JSON body content (read more here). Here is an example:

1
kind: Service
2
metadata:
3
name: first-service
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://example.com
9
authorization:
10
inlinePolicies:
11
- spec:
12
rules:
13
- effect: ALLOW
14
condition:
15
all:
16
of:
17
- match: ctx.request.http.method in ["GET", "POST", "PUT", "DELETE"]
18
- match: ctx.request.http.path.startsWith("/apis")
19
- match: ctx.request.http.uri == "/apis/users?name=john"
20
- match: ctx.request.http.queryParams.name == "john"
21
- match: ctx.request.http.headers["x-custom-header"] == "this-value"
22
- match: ctx.request.http.scheme == "http"
23
- match: string(ctx.request.http.body).toLower().contains("value1")
24
# JSON body map
25
- match: ctx.request.http.bodyMap.key1 == "value1"

Access control is the essence of the zero trust security model. This example is just the simplest use case of what you can do with Octelium's scalable, identity-based, fine-grained, context-aware, L7-aware access control system. Octelium supports defining Policy rules in Common Expression Language (CEL) and Open Policy Agent (OPA), rule priorities, nested conditions and extending access control by adding attributes to your different resources (e.g. Users, Groups, Services, etc...) from information provided by external tools such as IAM platforms, SIEM tools, threat intelligence tools, incident alerting and on-call management tools, etc.... You can read more in detail about Policies and access control here.

What Now?

This was just a quick guide to show you the main different features of Octelium. Octelium's architecture is designed to be flexible enough to be used as a Zero Trust Network Access (ZTNA) solution, a complete solution for secure tunnels, an API gateway (read more here), an AI gateway (read more here) and even can be used as a more advanced Kubernetes ingress/load balancer alternative as well as a self-hosted PaaS-like deployment and hosting platform to deploy, scale and provide secure or public anonymous access for your containerized applications such as Vite.js/Next.js/Astro web apps (see more here).

© 2025 octelium.comOctelium Labs, LLCAll rights reserved
Octelium and Octelium logo are trademarks of Octelium Labs, LLC.
WireGuard is a registered trademark of Jason A. Donenfeld