Zero trust Kubernetes

Kubernetes access without a kubeconfig

Octelium is a free and open source, self-hosted zero trust access platform. Its Kubernetes mode holds the kubeconfig, client certificate or bearer token, authorizes every API request on its verb, resource and namespace, and records what was called and by whom.

  • Free and open source
  • Designed for self-hosting
  • Works with ordinary kubectl
kubernetes.yaml
A Kubernetes ServiceOne kubeconfig lives in the Cluster and is never handed to a User.
A Kubernetes Service. One kubeconfig lives in the Cluster and is never handed to a User.
kind: Service
metadata:
  name: prod-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://k8s.internal:6443
    kubernetes:
      kubeconfig:
        fromSecret: kubeconfig-prod
A Policy. Each API request is authorized on its verb, resource and namespace.
kind: Policy
metadata:
  name: k8s-sre
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.request.kubernetes.verb == "get"
            - match: ctx.request.kubernetes.resource == "pods"
            - match: '"sre" in ctx.user.spec.groups'
A specific context. One kubeconfig with several contexts can be pinned to the one this Service uses.
kind: Service
metadata:
  name: staging-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://k8s-staging.internal:6443
    kubernetes:
      kubeconfig:
        context: staging
        fromSecret: kubeconfig-all
k8s-private.yaml
A private clusterAn API server behind NAT, reached over the tunnel through a single stable route.
A private cluster. An API server behind NAT, reached over the tunnel through a single stable route.
kind: Service
metadata:
  name: prod-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://k8s.internal:6443
    kubernetes:
      kubeconfig:
        fromSecret: kubeconfig-prod
The same cluster, clientlessly. One added field lets workloads reach the API over HTTPS with a standard bearer token.
kind: Service
metadata:
  name: prod-k8s
spec:
  mode: KUBERNETES
  isPublic: true
  config:
    upstream:
      url: https://k8s.internal:6443
    kubernetes:
      kubeconfig:
        fromSecret: kubeconfig-prod
A cluster in another network. The upstream is resolved from the side of a connected host inside that network.
kind: Service
metadata:
  name: edge-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://10.90.0.4:6443
      user: edge-gw
    kubernetes:
      kubeconfig:
        fromSecret: kubeconfig-edge
policy-verb.yaml
Verbs and namespacesThe verb, resource, namespace and API group are normalized request attributes.
Verbs and namespaces. The verb, resource, namespace and API group are normalized request attributes.
kind: Policy
metadata:
  name: read-only-prod
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.request.kubernetes.verb in ["get", "list"]
            - match: ctx.request.kubernetes.namespace == "production"
            - match: ctx.request.kubernetes.resource in ["pods"]
            - match: '"support" in ctx.user.spec.groups'
Down to a single object. The object name, subresource, API group and version are all part of the context.
kind: Policy
metadata:
  name: logs-only
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.request.kubernetes.verb == "get"
            - match: ctx.request.kubernetes.resource == "pods"
            - match: ctx.request.kubernetes.subresource == "log"
            - match: ctx.request.kubernetes.apiVersion == "v1"
An explicit denial. Deny rules are evaluated ahead of allow rules, which keeps an exception readable.
kind: Policy
metadata:
  name: no-secrets
spec:
  rules:
    - effect: DENY
      condition:
        match: ctx.request.kubernetes.resource == "secrets"
    - effect: ALLOW
      condition:
        match: '"engineers" in ctx.user.spec.groups'
secret.sh
A kubeconfigThe kubeconfig is created as a Secret from a file and never leaves the Cluster.
A kubeconfig. The kubeconfig is created as a Secret from a file and never leaves the Cluster.
octeliumctl create secret \
  --file /path/to/kubeconfig \
  kubeconfig-prod

# The Service then references it by name,
# and Users never receive the file itself.
A bearer token. A service account token can be used instead of a kubeconfig, with trusted CAs pinned.
kind: Service
metadata:
  name: prod-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://k8s.internal:6443
    kubernetes:
      bearerToken:
        fromSecret: k8s-bearer-token
A different identity per group. Group membership selects which upstream credential a request is proxied with.
kind: Service
metadata:
  name: prod-k8s
spec:
  mode: KUBERNETES
  config:
    upstream:
      url: https://k8s.internal:6443
    kubernetes:
      bearerToken:
        fromSecret: k8s-read-only-token
  dynamicConfig:
    configs:
      - name: admin
        upstream:
          url: https://k8s.internal:6443
        kubernetes:
          bearerToken:
            fromSecret: k8s-admin-token
    rules:
      - condition:
          match: '"sre" in ctx.user.spec.groups'
        configName: admin
idp.yaml
Secretless workload identityCI jobs authenticate with OIDC assertions issued by the platform running them.
Secretless workload identity. CI jobs authenticate with OIDC assertions issued by the platform running them.
kind: IdentityProvider
metadata:
  name: github-actions
spec:
  oidcIdentityToken:
    issuerURL: https://token.actions.githubusercontent.com
    audience: https://example.com
A workload User. A deployment pipeline is a User with its own groups, Policies and audit trail.
kind: User
metadata:
  name: deployer
spec:
  type: WORKLOAD
  groups:
    - ci
Services inside the cluster. A workload in the cluster can be published as an ordinary Service of its own.
kind: Service
metadata:
  name: argocd
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: http://argocd-server.argocd.svc:80
Visibility and auditing

Every API call, with the identity that made it

Every request produces an AccessLog identifying the User, Session and Device alongside the Kubernetes detail of the request itself, emitted in real time to your OpenTelemetry OTLP receivers.

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
octeliumctl
octeliumctl apply ./cluster
$ octeliumctl apply ./cluster
Secret: kubeconfig-prod Updated
Secret: k8s-admin-token Updated
IdentityProvider: okta Updated
Policy: read-only-prod Created
Group: sre Created
Group: support Updated
Service: prod-k8s Created
Service: staging-k8s Created
Service: argocd Created
User: deployer Created
Cluster Core resources successfully applied
 7 resources created
 3 resources updated

 ClusterConfig updated
The request

What a Kubernetes API request carries, and what checks it

A kubectl call is not one decision. These are the parts of a request that matter for access control and auditing, next to the mechanism in an Octelium Cluster that handles each one.

  1. 01

    Who is making the call

    The request carries an Octelium identity rather than a kubeconfig. People authenticate through your OpenID Connect or SAML 2.0 identity provider, and workloads through OAuth2 client credentials, access tokens or federated OIDC assertions.

    Read more
  2. 02

    The verb

    get, list, watch, create, update, patch and delete are normalized into the request context, so read access and write access to the same resource are separate decisions rather than one role binding.

    Read more
  3. 03

    The resource and its API group

    The resource, API group and API version are each available, so a policy can allow pods and services while leaving custom resources or cluster-scoped objects untouched.

    Read more
  4. 04

    The namespace

    The namespace is a normalized field, so the same Service can allow one group everything in staging and only reads in production, without maintaining parallel RBAC in each cluster.

    Read more
  5. 05

    The object and subresource

    The object name and subresource are available too, which is what makes rules such as reading pod logs without reading pod specs, or reading a status without patching it, expressible.

    Read more
  6. 06

    The underlying HTTP request

    Kubernetes is an HTTP API, and the full HTTP request is exposed alongside the normalized fields, including headers and paths, for the cases the normalized view does not cover.

    Read more
  7. 07

    The credential used upstream

    A kubeconfig, a client certificate or a bearer token is held as a Secret and used to authenticate to the API server after authorization. Which one is used can itself be selected per request from identity and context.

    Read more
  8. 08

    What ends up in the audit trail

    Each request produces an identity-aware AccessLog with the verb, resource, namespace, API version and the User behind it, streamed in real time over OpenTelemetry to your own log management and SIEM providers.

    Read more

RBAC still decides what the credential the Cluster holds may do. What changes is that nobody carries that credential, and the decision about who may use it is made per request against an identity you already manage.

Questions

Frequently asked

Does this replace Kubernetes RBAC?
No. RBAC still governs what the credential held by the Cluster is allowed to do inside the cluster. What changes is who holds that credential and how each request is decided: the kubeconfig, client certificate or bearer token stays in the Cluster, and every API call is authorized per request against an identity you already manage, before it reaches the API server.
How do people use kubectl?
With their ordinary kubectl. The Service is reached at a stable private name over the client-based tunnel, and the identity-aware proxy authenticates to the API server on the User's behalf. Nobody is issued a kubeconfig, a client certificate or a service account token of their own.
Which upstream credentials are supported?
Three. A kubeconfig stored as a Secret, optionally pinned to a specific context when the file contains several; a client certificate with its private key and trusted CAs; or a bearer token with trusted CAs. All three are referenced by Secret name from the Service definition.
Can workloads reach the API without a client?
Yes. A KUBERNETES Service can be published clientlessly, so an application authenticates with the standard OAuth2 client credentials flow or a bearer access token and calls the API over ordinary HTTPS, without a kubeconfig, a client or a proprietary SDK.
Does it work across several clusters?
Yes. Each cluster is its own Service with its own upstream credential and its own Policies, and a cluster in a network the Octelium Cluster cannot reach directly can be served from the side of a connected host inside that network with no inbound port. People reach all of them through the same single route and the same identity.
Do I need Kubernetes experience to run Octelium itself?
No. An Octelium Cluster runs on top of Kubernetes, but you do not need Kubernetes experience to install, operate or use it. 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.
Get started

Deploy Octelium on your own infrastructure in minutes

Free and open source. Self-hosted. No vendor lock-in.