Skip to content

Multi-Tenancy and Access Control

Tentacular is a multi-tenant workflow platform. Multiple teams share a single Kubernetes cluster, each owning an enclave and the tentacles deployed within it. Access control follows a familiar model: authenticate users via OIDC, authorize operations through POSIX-like RBAC, and account for every action through audit annotations and structured logging.

This guide covers the three pillars of the AAA (Authentication, Authorization, Accounting) framework that make multi-tenancy work.

Multi-tenancy AAA architecture showing authentication flow, RBAC enforcement, and audit trail

Authentication establishes who you are. Tentacular supports two authentication paths:

Users and agents authenticate via Keycloak using the OIDC Device Authorization Grant. Keycloak brokers identity from external providers (Google SSO, GitHub, etc.) and issues JWTs containing:

  • Subject (sub) — unique user identifier used for ownership matching
  • Email — display identity

Group membership from the IdP (Keycloak groups) is not used for authorization. Enclave membership is derived from Slack channel membership, not directory groups. This keeps the group model self-service and removes dependency on enterprise directory structures.

Terminal window
# Authenticate via OIDC
tntc login
# Verify your identity
tntc whoami

Service accounts and automation tools authenticate with a bearer token. Bearer-token requests bypass all RBAC checks — this is the admin/automation escape hatch for clusters without SSO or for break-glass operations.

Authorization determines what you can do. Tentacular implements RBAC using a POSIX filesystem permission model — the same owner/group/mode semantics that Unix administrators already know.

Enclaves are tenant boundaries. Each team owns one enclave, and the enclave owner controls who can operate within it. Tentacles deployed inside an enclave are further protected by their own permissions.

Think of it as a filesystem:

  • Enclaves = directories — tenant boundaries with their own owner, members, and mode
  • Tentacles = files — individual resources with their own owner and mode
  • Both checks must pass — just like accessing /team/app.sh requires directory read + file read

Enclave authorization model showing directory/file permission analogy and two-layer check flow

ScopeWhoHow Determined
OwnerThe user identified by tentacular.io/owner-subThe person who provisioned the enclave or deployed the tentacle
MemberRegistered enclave membersUsers who joined the Slack channel and completed OIDC sign-in
OtherAny other authenticated userAuthenticated users who are not the owner or a registered member

IdP group membership (Keycloak groups, LDAP groups) is not used for authorization. The tentacular.io/group annotation is deprecated — enclave membership is the authorization primitive.

PermissionBitOperations
Read (r)4List, status, describe, health, logs, pods, events
Write (w)2Deploy, update, remove, set permissions
Execute (x)1Run, restart

Mode is stored as a 9-character rwx string (e.g., rwxrwx---). Each group of three characters represents one scope (owner, member, other):

Mode StringOwnerMemberOtherMeaning
rwxr-x---rwxr-x---Owner full access, members can read and run, others blocked
rwx------rwx------Owner only — private
rwxrwx---rwxrwx---Owner and members have full access (default)
rwx--x---rwx—x---Owner full access, members can execute only
rwxrwxr--rwxrwxr—Owner and members full access, others can view
rwxrwxr-xrwxrwxr-xOwner and members full access, others can view and run

Named presets map to common access patterns. Use preset names with enclave_sync and tntc permissions chmod. The presets reflect enclave membership (member = registered enclave member, other = unenrolled authenticated user):

PresetMode StringUse Case
privaterwx------Owner only — personal or sensitive work
member-readrwxr-x---Members can view and run; only owner deploys
member-runrwx--x---Members can only execute, not inspect
member-editrwxrwx---Default — full collaboration within the team
open-readrwxrwxr--Visitors (non-members) can view; members have full access
open-runrwxrwxr-xVisitors can view and trigger; members have full access

The default mode for new enclaves and new tentacle deployments is member-edit (rwxrwx---).

The MCP server is the single enforcement point. Every OIDC-authenticated request passes through two permission checks:

  1. Enclave check — does the caller have the required permission on the enclave?
  2. Tentacle check — does the caller have the required permission on the tentacle?

Both must pass. A user with enclave read but no tentacle read cannot describe a tentacle. A user with tentacle write but no enclave read cannot even reach the tentacle. The enclave owner is a superuser within their enclave — they bypass tentacle-level checks and can perform any operation on any tentacle.

POSIX OperationTentacular EquivalentRequired Permission
ls /team/wf_list in enclaveEnclave Read
touch /team/app.shwf_apply (create tentacle)Enclave Write
cat /team/app.shwf_describe on tentacleEnclave Read + Tentacle Read
./team/app.shwf_run on tentacleEnclave Read + Tentacle Execute
rm /team/app.shwf_remove on tentacleEnclave Read + Tentacle Write

Enclaves carry the same permission model as tentacles and serve as the tenant boundary:

BitOperations
Read (r)wf_list, enclave_info, wf_health, wf_pods, wf_logs, wf_events
Write (w)wf_apply (deploy new tentacle), enclave_sync settings
Execute (x)Run tentacles within the enclave

When enclave_provision is called, the caller becomes the enclave owner. The enclave namespace receives annotations:

  • tentacular.io/owner-sub, owner-email, owner-name — from OIDC identity
  • tentacular.io/enclave-members — JSON array of registered member emails
  • tentacular.io/mode — permission mode string (default: rwxrwx---)

The tentacular.io/group annotation is deprecated and not used for authorization.

Enclaves specify defaults for new tentacles deployed within them:

  • tentacular.io/default-mode — default mode for new tentacles (e.g., rwxrwx---)

Accounting tracks who did what and when. Tentacular records audit data in two places: Kubernetes annotations on resources and structured slog output.

Authorization metadata is stored as Kubernetes annotations on Deployment resources and Namespace resources. All annotations use the tentacular.io/ prefix.

Ownership Annotations (stamped on CREATE only)

Section titled “Ownership Annotations (stamped on CREATE only)”

These are set when a tentacle is first deployed and preserved on subsequent updates. This prevents ownership takeover on redeploy.

AnnotationDescription
tentacular.io/owner-subOwner’s OIDC subject identifier (used for identity matching)
tentacular.io/owner-emailOwner’s email address (display)
tentacular.io/owner-nameOwner’s display name (display)
tentacular.io/modePermission string (e.g., rwxrwx---)
tentacular.io/auth-providerAuthentication provider used at deploy time (e.g., keycloak, bearer-token)
tentacular.io/created-atCreation timestamp (set once on first deploy)

Enclave namespaces additionally carry:

AnnotationDescription
tentacular.io/enclave-ownerEnclave owner email
tentacular.io/enclave-owner-subEnclave owner OIDC subject identifier
tentacular.io/enclave-membersJSON array of registered member emails
tentacular.io/channel-idPlatform channel ID (e.g., Slack channel ID)
tentacular.io/channel-namePlatform channel display name

The tentacular.io/group annotation is deprecated. It may be present on pre-enclave deployments but is not used by the authorization evaluator.

Update Tracking Annotations (stamped on UPDATE)

Section titled “Update Tracking Annotations (stamped on UPDATE)”

These are updated on every subsequent deploy to track who last modified the tentacle.

AnnotationDescription
tentacular.io/updated-atLast update timestamp
tentacular.io/updated-by-subLast updater’s OIDC subject identifier
tentacular.io/updated-by-emailLast updater’s email address

These annotations are always stamped on Deployments regardless of authz configuration:

AnnotationDescription
tentacular.io/deployed-byDeployer email
tentacular.io/deployed-viaAgent type (e.g., cli)
tentacular.io/deployed-atDeployment timestamp

The MCP server emits structured slog entries for every authorization decision. These logs integrate with standard Kubernetes log aggregation (Loki, Fluentd, CloudWatch, etc.) for centralized audit trails.

The following tentacular.dev/* annotations have been replaced:

Old AnnotationReplacement
tentacular.dev/ownertentacular.io/owner-sub, tentacular.io/owner-email, tentacular.io/owner-name
tentacular.dev/teamDeprecated — use enclave membership instead
tentacular.dev/environmenttentacular.io/environment
tentacular.dev/tagstentacular.io/tags
tentacular.dev/cron-scheduletentacular.io/cron-schedule

The old tentacular.dev/* annotations are no longer recognized. Existing deployments using old annotations must be redeployed to receive authorization metadata.

The tentacular.io/group annotation and the --group/--share group flag are deprecated. Authorization now uses enclave membership (owner/member/other) instead of IdP group assignment. Existing deployments with tentacular.io/group set will have the annotation ignored by the authorization evaluator — access is controlled by enclave membership only.

Terminal window
# --- Tentacle permissions (2 positional args: enclave + tentacle name) ---
# Check permissions on a tentacle
tntc permissions get <enclave> <name>
# Set mode using a preset name or rwx string
tntc permissions chmod member-read <enclave> <name>
tntc permissions chmod rwxr-x--- <enclave> <name>
# Deploy with member-readable mode
tntc deploy --share --enclave <enclave>
# --- Enclave permissions (1 positional arg: enclave name) ---
# Check permissions on an enclave
tntc permissions get <enclave>
# Set enclave mode
tntc permissions set <enclave> --mode open-read
# Shortcuts
tntc chmod <mode-or-preset> <enclave>

Only the owner can modify permissions on a tentacle or enclave. The --group and chgrp flags are removed — member access is governed by enclave membership (Slack channel membership), not group assignment.

Authorization is enabled by default when the MCP server starts. To disable all authorization checks, set the environment variable:

Terminal window
TENTACULAR_AUTHZ_ENABLED=false

When disabled, all authenticated requests (OIDC or bearer token) have full access to all operations. The default mode for new enclaves and tentacles is member-edit (rwxrwx---).

ToolDescription
enclave_provisionProvision a new enclave with ownership and membership
enclave_syncUpdate enclave membership, status, or mode
enclave_infoShow enclave details, membership, and quota
enclave_listList accessible enclaves
enclave_deprovisionPermanently delete an enclave (irreversible)

See Enclave MCP Tools Reference for full parameter details.

Resources without a tentacular.io/owner-sub annotation are denied to all OIDC callers. This prevents accidental access to resources that were deployed before authorization was enabled or whose ownership annotations were removed.

To adopt unowned resources, use bearer-token authentication or kubectl (see below).

MCP-layer authorization enforces permissions through Kubernetes annotations. Cluster administrators with kubectl access can directly manage these annotations as a break-glass mechanism.

Terminal window
# Stamp ownership on a tentacle
kubectl annotate deploy -n my-enclave my-tentacle \
tentacular.io/owner-sub=<user-uuid> \
tentacular.io/owner-email=user@example.com \
tentacular.io/owner-name="User Name" \
tentacular.io/mode=rwxrwx---
# Stamp ownership on an enclave namespace
kubectl annotate ns my-enclave \
tentacular.io/owner-sub=<user-uuid> \
tentacular.io/owner-email=user@example.com \
tentacular.io/owner-name="User Name" \
tentacular.io/enclave-members='["member1@example.com","member2@example.com"]' \
tentacular.io/mode=rwxrwx---

Find user UUIDs via tntc whoami (Subject field) or the Keycloak admin console.

Terminal window
kubectl annotate deploy -n my-enclave my-tentacle \
tentacular.io/owner-sub=<new-uuid> \
tentacular.io/owner-email=new@example.com \
tentacular.io/owner-name="New Owner" \
--overwrite
Terminal window
kubectl get deploy -n my-enclave -o custom-columns=\
NAME:.metadata.name,\
OWNER:.metadata.annotations.tentacular\.io/owner-email,\
MODE:.metadata.annotations.tentacular\.io/mode