AlmaForge Kubernetes Access Guide
Connect Kubernetes clusters to AlmaForge for SSO-backed access through
kubectl. Users receive short-lived credentials and mapped Kubernetes
identities. The agent reaches private API servers while Kubernetes
RBAC continues to control what those identities can do.
- Unify access across all Kubernetes clusters. Put EKS, GKE, AKS, and self-hosted clusters behind one access path instead of managing separate access workflows for each platform.
- SSO mapped into Kubernetes RBAC, with no static credentials.
Users sign in with SSO, AlmaForge maps identity to Kubernetes
users and groups.
kubectlobtains short-lived credentials through the exec plugin instead of a static kubeconfig or long-lived token, while the Kubernetes API server still makes the final allow-or-deny decision through its own bindings. See RBAC. - Keep existing tools. Engineers keep
kubectl, kubeconfig, and the rest of their normal Kubernetes workflow. No retraining, no separate Kubernetes client, and no separate operator path for day-to-day work. The AlmaForge CLI supplies credentials tokubectl. See Connect to Kubernetes. - Expose private clusters and internal services through managed access. The agent dials out from the cluster or VM, so Kubernetes APIs, in-cluster services, internal applications, and databases stay off the public internet while remaining reachable through one controlled access path.
- Record what actually happened. Every Kubernetes request is tied
to a human or workload identity. Interactive
kubectl execandattachterminals support playback. Audit events capture non-interactive commands and port-forward activity. Capture follows your recording and audit policy.
Prerequisites
- A running AlmaForge cluster with Auth Service and Proxy Service
reachable at
almaforge.example.com:443. - An SSO connector configured on that cluster, with at least one operator account that can sign in through the Proxy. See Single Sign-On for OIDC setup.
- The
almaCLI installed on an operator workstation with network access to the Proxy. See Install the CLI. The operator account must be able to applyKubernetesTarget,Role, and join-token resources. Theadminpreset role covers all three. - A target Kubernetes cluster running version 1.24 or later, with cluster-admin access for the initial RBAC setup.
kubectl1.24 or later on operator workstations.- For the remote cluster path, a Linux VM with
almadalready installed, its systemd unit already installed, and network reachability to both the target cluster API server and the Proxy atalmaforge.example.com:443. See Install AlmaForge.
In-cluster agent
There are two ways to connect a Kubernetes cluster to AlmaForge:
- Run the agent inside the cluster.
- Run the agent outside the cluster and point it at the cluster with a kubeconfig.
The in-cluster path is the recommended default, and this section covers it. If you need to run the agent outside the cluster, for example on a VM, use Remote cluster.
almad runs inside the Kubernetes cluster you want to protect and connects out to your AlmaForge Proxy Service. From there it can broker access to the Kubernetes API itself, with fine-grained control per namespace, resource, and verb, and to applications, databases, or other services that stay reachable only over in-cluster networking. Nothing needs to be exposed externally. The protected targets stay inside Kubernetes, and the agent is the path back to them.
The next steps set up the agent and bring this cluster online in
AlmaForge. The RBAC section later is about user access:
which users can reach this cluster and what they can do with
kubectl.
Enroll from the web UI instead
The steps below apply each manifest by hand, which is the path to take
when you want to read or adapt them. To have them generated instead,
open Resources, choose Enroll resource, and pick Kubernetes.
The wizard issues the join token, then gives you two commands: one
applies the manifests the Proxy serves at /install.yaml, the other
applies this cluster's own Secret and KubernetesTarget.
The served manifests carry the scoped role from Step 3, not the quick-start cluster-admin binding, and they are the same for every cluster. The wizard then waits until the cluster registers, so you see it join rather than checking by hand.
Step 1. Verify in-cluster connectivity
Connect to the target cluster with your existing cluster-admin path. AlmaForge is not in front of this cluster yet, so use the cloud provider's credential flow or whatever native admin kubeconfig you already use:
- GKE:
gcloud container clusters get-credentials - EKS:
aws eks update-kubeconfig - AKS:
az aks get-credentials --admin - Other clusters: an emergency admin kubeconfig, client certificate, or pre-positioned cluster-admin credential.
This is the same path described in Operations > Break-glass access. Keep it working after install. It is how you recover the cluster if the agent or RBAC ever get into a bad state.
Confirm the kubeconfig works and has the permissions this guide needs:
kubectl cluster-infokubectl auth can-i create namespacekubectl auth can-i create clusterrolebindingcluster-info proves the API server is reachable and the current
context is valid. The two can-i checks confirm you have the
cluster-admin scope required to apply the rest of the manifests.
Step 2. Create the in-cluster Namespace
Namespaced agent objects live in one namespace. Apply it first. ClusterRoles and ClusterRoleBindings are cluster-scoped and remain outside that namespace.
apiVersion: v1
kind: Namespace
metadata:
name: almaforge
Step 3. Create the ServiceAccount
The ServiceAccount needs a ClusterRole for impersonation,
resource discovery, exec/attach proxying, and access to its own
state Secrets. The Kubernetes Secret backend persists per-pod
identity in <pod>-state Secrets in the agent's namespace, so
Secret CRUD on that namespace is required.
Pick the quick-start path for a fast setup, or the production path for least-privilege scoping. The user-side group bindings live in RBAC.
- Quick-start
- Production
Grants the agent cluster-admin. One binding covers
impersonation, discovery, and state Secret access.
apiVersion: v1
kind: ServiceAccount
metadata:
name: almaforge
namespace: almaforge
---
# Quick-start RBAC: grants the agent cluster-admin so it can
# impersonate any user or group, read any resource, and manage its
# own state Secrets. For least-privilege bindings, use
# 02-rbac-advanced.yaml. See the Kubernetes RBAC section of the
# docs for the user-side group bindings.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: almaforge-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: almaforge
namespace: almaforge
Uses a scoped ClusterRole with only what the agent needs (impersonate, pod/node discovery, exec/attach proxying, RBAC discovery, SAR preflight) plus a namespace-scoped Role for state Secret CRUD.
# Production RBAC: a scoped ClusterRole granting only what the
# agent actually needs, plus a namespace-scoped Role for the
# Kubernetes Secret backend that stores per-pod identity.
# Mirrors the reference deployment in modules/almaforge-k8s/.
apiVersion: v1
kind: ServiceAccount
metadata:
name: almaforge
namespace: almaforge
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: almaforge
rules:
# Impersonate AlmaForge users as Kubernetes users, groups, or SAs.
- apiGroups: ['']
resources: ['users', 'groups', 'serviceaccounts']
verbs: ['impersonate']
# Discover and watch pods and nodes.
- apiGroups: ['']
resources: ['pods', 'nodes']
verbs: ['get', 'list', 'watch']
# Proxy exec, attach, and port-forward.
- apiGroups: ['']
resources: ['pods/exec', 'pods/attach', 'pods/portforward']
verbs: ['create']
# Validate own permissions at startup.
- apiGroups: ['authorization.k8s.io']
resources: ['selfsubjectaccessreviews']
verbs: ['create']
# Discover RBAC objects for access mapping.
- apiGroups: ['rbac.authorization.k8s.io']
resources: ['clusterroles', 'clusterrolebindings', 'roles', 'rolebindings']
verbs: ['get', 'list', 'watch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: almaforge
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: almaforge
subjects:
- kind: ServiceAccount
name: almaforge
namespace: almaforge
---
# Namespace-scoped access to <pod>-state Secrets used by the
# Kubernetes Secret backend.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: almaforge-state-backend
namespace: almaforge
rules:
- apiGroups: ['']
resources: ['secrets']
verbs: ['get', 'create', 'patch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: almaforge-state-backend
namespace: almaforge
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: almaforge-state-backend
subjects:
- kind: ServiceAccount
name: almaforge
namespace: almaforge
Step 4. Create the ConfigMap with the KubernetesTarget
The agent reads AlmaForge resource YAML from /etc/almaforge/almad.d. The
ConfigMap mounts at that path, where each data key becomes a resource
file. The agent loads KubernetesTarget files at startup. Restart the
agent after changing those targets. On a VM, the equivalent is a YAML
file in /etc/almaforge/almad.d.
# Drop-in resources for the in-cluster agent. This ConfigMap mounts at
# /etc/almaforge/almad.d, where almad reads resource files and
# hot-reloads on change. No restart needed.
apiVersion: v1
kind: ConfigMap
metadata:
name: almaforge-resources
namespace: almaforge
data:
# Registers this Kubernetes cluster. metadata.name is the cluster
# name users type into `alma kube login`. metadata.labels
# are what AlmaForge role matchers and ALMA_MATCH_LABELS evaluate
# against. Empty spec means "use the Pod's mounted ServiceAccount
# token".
kubernetes.yaml: |
apiVersion: almaforge.com/v1
kind: KubernetesTarget
metadata:
name: in-cluster
labels:
env: dev
region: local
spec: {}
# Optional: register an in-cluster HTTP service as an AppTarget so
# users can reach it through the AlmaForge proxy with `alma app
# login <app-name>`. Requires ALMA_APP_SERVICE=true on the agent
# (set in 04-env-secret.yaml).
#
# app.yaml: |
# apiVersion: almaforge.com/v1
# kind: AppTarget
# metadata:
# name: <app-name>
# labels:
# app: <app-name>
# spec:
# uri: http://<service>.<namespace>.svc.cluster.local:<port>
metadata.name becomes the cluster name users type into
alma kube login. metadata.labels are arbitrary
key/value pairs. There is no required schema. The example uses
env and region, but you can choose any labels that fit your
role matchers. spec: {} means "use the agent's ambient
Kubernetes credentials". In this deployment that means the pod's
mounted ServiceAccount token. For a remote agent it means the
kubeconfig at ALMA_KUBECONFIG.
The same ConfigMap also includes a commented-out AppTarget
example for registering an in-cluster HTTP service. Uncomment
and customize it to expose an internal app through the same
agent. For the full application flow, see
Applications access.
Step 5. Issue a join token
A join token authenticates the agent to the AlmaForge Auth Service the first time it starts. Its method block decides how the agent proves identity: a static random string, or a workload identity such as AWS IAM, a GCP service account, or an Azure managed identity. Workload-identity tokens carry no secret material, but they require the Auth Service to trust the cloud provider's identity signal, so they fit clusters running on a supported cloud.
Pick the join method that matches where the agent runs.
- Static token
- AWS IAM (EKS)
- GCP (GKE)
- Azure (AKS)
A random string with a TTL. Useful for first install and for clusters without a supported workload-identity provider.
alma create token --services=kubernetes,app,database --format=textDefault TTL is 30 minutes. The token is not single-use: both
StatefulSet replicas can consume the same token. Each trades it
for its own per-pod identity, persisted in <pod>-state Secrets.
Once both replicas have joined, the token is no longer used.
Override the TTL with --expires=24h for slower rollouts. Revoke
a leaked or unused token with alma delete token/<client-id>.
Pass --client-id=<name> to alma create token to set the
resource name explicitly. With --format=text, the command prints client_id on the first line
and client_secret on the second. Set ALMA_CLIENT_ID and ALMA_CLIENT_SECRET on
the agent Pod (e.g., in the almaforge-env Secret).
The example environment enables Kubernetes, App, and Database Services,
so the token grants all three. For a Kubernetes-only agent, use
--services=kubernetes and disable the App and Database Services in
the environment Secret.
The agent proves identity via its IRSA service account or EC2 instance profile. Configure IRSA (or attach an instance profile) before applying the token.
kube-iam.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: kube-agent
spec:
roles:
- system:kubernetes
aws:
allow:
- awsAccount: '123456789012'
awsARN: 'arn:aws:sts::123456789012:assumed-role/almaforge-agent/*'
Set ALMA_CLIENT_ID to the token name. Pin awsAccount and
awsARN to the exact role the agent assumes. Wildcards in
awsARN widen the trust boundary.
The bundled environment enables App and Database Services too. Add
system:app and system:database to spec.roles to use that environment,
or disable those services for a Kubernetes-only agent.
The agent proves identity via the GCE/GKE metadata server. Configure GKE Workload Identity to bind the agent's Kubernetes ServiceAccount to a Google service account before applying the token.
kube-gcp.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: kube-agent
spec:
roles:
- system:kubernetes
gcp:
allow:
- projectIDs:
- 'my-project'
serviceAccounts:
- 'agent@my-project.iam.gserviceaccount.com'
Set ALMA_CLIENT_ID to the token name.
The bundled environment enables App and Database Services too. Add
system:app and system:database to spec.roles to use that environment,
or disable those services for a Kubernetes-only agent.
The agent proves identity via its Azure managed identity. Assign the managed identity to the agent's pod (Azure AD Workload Identity) before applying the token.
kube-azure.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: kube-agent
spec:
roles:
- system:kubernetes
azure:
allow:
- subscription: '11111111-1111-1111-1111-111111111111'
resourceGroups:
- 'aks-rg'
Set ALMA_CLIENT_ID to the token name.
The bundled environment enables App and Database Services too. Add
system:app and system:database to spec.roles to use that environment,
or disable those services for a Kubernetes-only agent.
Step 6. Create the Secret with environment variables
The StatefulSet loads a single Secret with envFrom, so every key
becomes a container environment variable. Use it to pass the proxy
address, the join token reference, and the per-role toggles in one
place. In the example file, the required keys are:
ALMA_PROXY: theAlmaForgeProxy address.ALMA_CLIENT_ID: theclient_idprinted byalma create tokenin Step 5.ALMA_CLIENT_SECRET: theclient_secretprinted byalma create tokenin Step 5, used only by the static-token method. Workload-identity methods (kubernetes, AWS, GCP, Azure) carry no secret and instead setALMA_JOIN_METHODwith justALMA_CLIENT_ID, as the example file does.ALMA_KUBERNETES_SERVICE=true: turns on the Kubernetes Service role.
The bundled Secret uses Kubernetes-native joining to a Proxy in the
same Kubernetes cluster. For the static-token path above, set
ALMA_PROXY to your Proxy address, set ALMA_JOIN_METHOD=static, and
replace ALMA_CLIENT_ID and ALMA_CLIENT_SECRET with the issued values.
For Kubernetes-native joining, configure the
Kubernetes join method and its
Token before deploying the agent.
# Bundles the join configuration with the rest of the agent's
# environment. The StatefulSet loads this Secret with envFrom, so
# every key becomes a container environment variable.
apiVersion: v1
kind: Secret
metadata:
name: almaforge-env
namespace: almaforge
stringData:
# AlmaForge Proxy address (host:port). The agent connects here
# over mTLS and registers itself with the Auth Service through
# the same socket. This points at the in-cluster proxy headless
# Service published by the reference cluster bundle.
ALMA_PROXY: 'almaforge.almaforge-cluster.svc:8443'
# Kubernetes join method: the agent proves identity with its
# mounted ServiceAccount JWT, which the Auth Service validates
# through the Kubernetes TokenReview API. No shared secret is
# carried, so only ALMA_CLIENT_ID is set (the Token resource's
# metadata.name). The kube-agent Token shipped by the cluster
# bundle allows the almaforge:almaforge ServiceAccount created in
# 02-rbac-advanced.yaml.
ALMA_JOIN_METHOD: 'kubernetes'
ALMA_CLIENT_ID: 'kube-agent'
# Static-token join method instead: drop ALMA_JOIN_METHOD above,
# run `alma create token --services=kubernetes,app,database` to mint
# a token, set ALMA_CLIENT_ID to the printed client_id, and uncomment
# ALMA_CLIENT_SECRET below with the printed client_secret.
# ALMA_CLIENT_SECRET: '<paste-client-secret-here>'
# The proxy serves a web TLS leaf minted by the cluster's private
# cert-manager CA. trust-manager distributes that CA into every
# namespace as the almaforge-ca ConfigMap. Mount it (see
# 05-statefulset.yaml) and point SSL_CERT_FILE at the bundle so
# the agent trusts the proxy on first connect.
SSL_CERT_FILE: '/etc/ssl/almaforge/ca-bundle.pem'
# Enable the Kubernetes Service role: brokers kubectl traffic to
# the in-cluster API server using the Pod's ServiceAccount.
ALMA_KUBERNETES_SERVICE: 'true' # Sic: only strings are supported here.
# Enable the Application Service role: brokers HTTP(S) access to
# in-cluster apps and services declared as AppTarget resources.
ALMA_APP_SERVICE: 'true'
# Enable the Database Service role: brokers connections to
# in-cluster databases declared as DatabaseTarget resources.
ALMA_DATABASE_SERVICE: 'true'
# SSH is not needed on an in-cluster agent.
ALMA_SSH_SERVICE: 'false'
# Resource label selector, fanned out to the app, database, and
# Kubernetes services. Without it those services start but watch no
# dynamic targets, so AppTarget / DatabaseTarget / KubernetesTarget
# resources registered in the cluster are never brokered. '*=*' makes
# this agent serve every target in the cluster. Narrow it (for
# example env=prod) to shard targets across several agents.
ALMA_MATCH_LABELS: '*=*'
# Diagnostic endpoint listen address. Required for the
# `/healthz` and `/readyz` probes the StatefulSet uses.
ALMA_DIAG_LISTEN: '0.0.0.0:3000'
Step 7. Create the StatefulSet
The agent runs as a StatefulSet so each replica gets a stable pod
name (almaforge-0, almaforge-1). The Kubernetes Secret backend
activates automatically when the agent detects an in-cluster
ServiceAccount token. It stores per-pod identity in <pod>-state
Secrets keyed off the pod's hostname, so stable names mean stable
host UUIDs across restarts. No env vars are required to wire it up.
A headless Service is required by the StatefulSet spec.
The container image is quay.io/almaforge/almaforge:2026.1.26, which resolves to
quay.io/almaforge/almaforge:<version> at docs build time. For
production, pin the image to a sha256 digest:
crane digest quay.io/almaforge/almaforge:2026.1.26sha256:...Then set image: quay.io/almaforge/almaforge:2026.1.26@sha256:... in the
StatefulSet. For private registries, add an imagePullSecrets
entry referencing your registry credentials Secret.
# Headless Service is required by the StatefulSet spec for stable
# pod DNS. Mirrors modules/almaforge-k8s/agent.tf.
apiVersion: v1
kind: Service
metadata:
name: almaforge
namespace: almaforge
spec:
clusterIP: None
selector:
app.kubernetes.io/instance: almaforge
ports:
- name: diag
port: 3000
---
# StatefulSet (not Deployment) so each replica gets a stable pod
# name (almaforge-0, almaforge-1). The Kubernetes Secret backend
# keys per-pod identity off the pod's hostname, so stable names mean
# stable host UUIDs across restarts.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: almaforge
namespace: almaforge
spec:
replicas: 2
serviceName: almaforge
selector:
matchLabels:
app.kubernetes.io/instance: almaforge
template:
metadata:
labels:
app.kubernetes.io/name: almaforge
app.kubernetes.io/instance: almaforge
app.kubernetes.io/component: agent
spec:
serviceAccountName: almaforge
automountServiceAccountToken: true
# Spread replicas across nodes (and zones if available) so a
# single-node failure cannot take both pods. ScheduleAnyway
# keeps the StatefulSet schedulable on small/dev clusters.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: almaforge
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: almaforge
containers:
- name: almaforge
# For production, pin the image to an immutable digest:
# image: quay.io/almaforge/almaforge:latest@sha256:<digest>
image: quay.io/almaforge/almaforge:latest
imagePullPolicy: IfNotPresent
ports:
- name: diag
containerPort: 3000
envFrom:
- secretRef:
name: almaforge-env
volumeMounts:
- name: config
mountPath: /etc/almaforge/almad.d
readOnly: true
- name: cluster-ca
mountPath: /etc/ssl/almaforge
readOnly: true
resources:
requests:
cpu: '100m'
memory: '256Mi'
limits:
memory: '512Mi'
livenessProbe:
httpGet:
path: /healthz
port: diag
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /readyz
port: diag
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: config
configMap:
name: almaforge-resources
# CA bundle trust-manager distributes into every namespace.
# Lets the agent trust the proxy's cert-manager web TLS leaf
# on first connect (see SSL_CERT_FILE in 04-env-secret.yaml).
- name: cluster-ca
configMap:
name: almaforge-ca
---
# PodDisruptionBudget keeps at least one replica serving during
# voluntary disruptions (node drains, cluster upgrades, autoscaler
# moves). Pair with topologySpreadConstraints above so the kept
# replica is on a different node from the one being drained.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: almaforge
namespace: almaforge
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/instance: almaforge
Step 8. Apply the manifests and verify
The previous steps walked through the manifests one by one for
explanation. To deploy, save them into one directory and apply
everything at once. Kubernetes applies them in lexical filename
order, which is why the prefixes 01- through 05- matter:
kubectl apply -f .Wait for both replicas to roll out and become Ready before
treating the deployment as live. kubectl apply returning
success only means the API accepted the objects, not that the
agent is serving:
kubectl -n almaforge rollout status statefulset/almaforge --timeout=120skubectl -n almaforge get pods -l app.kubernetes.io/instance=almaforgeNAME READY STATUS RESTARTS AGEalmaforge-0 1/1 Running 0 90salmaforge-1 1/1 Running 0 80sConfirm each replica completed the join handshake and the
SelfSubjectAccessReview preflight:
kubectl -n almaforge logs -l app.kubernetes.io/instance=almaforge --tail=50 | \ grep -Ei 'joined|registered|impersonate'Then confirm AlmaForge sees the cluster:
alma get kubeCluster Labels Version-------------- ---------------------------- -------prod-eu-central env=prod,region=eu-central 2026.1.26Run an end-to-end check from a workstation logged in to AlmaForge. A successful access shows up in the audit trail:
alma kube login <cluster-name>kubectl get pods -AConfirm the request in the audit event stream.
To verify terminal recording as well, open and close an authorized
kubectl exec -it session, then list recordings with
alma recording ls --last=10m.
The cluster can take a short time to appear. Restart the StatefulSet after changing file-mounted targets. After the cluster appears and both replicas are Ready, revoke the static join token:
alma delete token/<client-id>Workload-identity tokens (IAM/GCP/Azure) stay in place because the agent re-authenticates from ambient credentials on every rejoin. See Replica lifecycle for when a fresh token is needed again.
Remote cluster
Use this mode when the agent runs outside the target cluster. The recommended in-cluster path is in In-cluster agent.
almad is deployed outside the Kubernetes cluster and reaches the target cluster's API server over the network using a kubeconfig file. One agent can hold kubeconfigs for many clusters and serve them all at once. The kubeconfig provides access to the Kubernetes API, not a network tunnel to every in-cluster service. Use it for managed clusters where you don't control Pods, fleets of small clusters served by a shared agent, or deliberate separation of duties. Publishing in-cluster apps or databases requires their own enabled service and network reachability from the agent. A kubeconfig alone does not provide that network path.
The examples below assume the agent runs on a Linux VM with systemd.
At a high level, the remote setup has four parts: cluster-side
impersonator RBAC, a kubeconfig file on the VM that uses that
identity, an env file with the join token and kubeconfig path, and a
KubernetesTarget resource that registers the cluster in
AlmaForge. User access comes later in RBAC.
Step 1. Verify remote cluster connectivity
Same checks as Step 1 of the in-cluster agent setup, run against every target cluster the agent will serve.
Step 2. Create the remote Namespace
Same shape as Step 2 of the in-cluster agent setup, applied to each target cluster.
01-namespace.yaml, applied to each target cluster:
apiVersion: v1
kind: Namespace
metadata:
name: almaforge
Step 3. Create the ServiceAccount and impersonator RBAC
The VM-hosted agent still needs an identity inside each target
cluster. Give it a ServiceAccount and a ClusterRole that can
impersonate users and groups, run the startup preflight, and proxy
Kubernetes subresources such as exec, attach, and
port-forward.
02-rbac.yaml, applied to each target cluster:
# ServiceAccount and impersonator RBAC for the remote-agent path.
# Apply this to each target cluster the VM-hosted agent will serve.
apiVersion: v1
kind: ServiceAccount
metadata:
name: almaforge
namespace: almaforge
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: almaforge-impersonator
rules:
- apiGroups: ['']
resources: ['users', 'groups', 'serviceaccounts']
verbs: ['impersonate']
- apiGroups: ['']
resources: ['pods', 'nodes']
verbs: ['get', 'list', 'watch']
- apiGroups: ['']
resources: ['pods/exec', 'pods/attach', 'pods/portforward']
verbs: ['create']
- apiGroups: ['authorization.k8s.io']
resources: ['selfsubjectaccessreviews']
verbs: ['create']
- apiGroups: ['rbac.authorization.k8s.io']
resources: ['clusterroles', 'clusterrolebindings', 'roles', 'rolebindings']
verbs: ['get', 'list', 'watch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: almaforge-impersonator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: almaforge-impersonator
subjects:
- kind: ServiceAccount
name: almaforge
namespace: almaforge
The user-side group bindings still live in RBAC.
Step 4. Build the kubeconfig
Build a kubeconfig that authenticates as the almaforge
ServiceAccount you just created. The file needs three values:
- The target cluster's API server address.
- The target cluster's CA certificate in base64 form.
- A token for the
almaforgeServiceAccount.
Get those values from a workstation already pointed at the target cluster:
kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.server}'kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}'kubectl -n almaforge create token almaforge03-kubeconfig.yaml, a template to copy to the VM and fill in:
# Kubeconfig used by the VM-hosted Kubernetes agent.
# Replace the placeholders with the target cluster's API server,
# CA certificate, and the ServiceAccount token from Step 2.
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: <base64-cluster-ca>
server: https://<api-server-hostname>:6443
name: <cluster-name>
contexts:
- context:
cluster: <cluster-name>
user: almaforge
name: <cluster-name>
current-context: <cluster-name>
kind: Config
preferences: {}
users:
- name: almaforge
user:
token: <service-account-token>
Copy 03-kubeconfig.yaml to prod.yaml and replace the three
placeholders with the API server address, CA data, and
ServiceAccount token you just retrieved.
This path is simple, but the token from kubectl create token is
not a permanent credential. The API server decides its lifetime. For
long-lived VM agents, plan to rotate that token before it expires or
use an exec-based kubeconfig instead.
For managed clusters, the kubeconfig may also use an IAM-backed
exec plugin (aws-iam-authenticator, gke-gcloud-auth-plugin,
kubelogin) instead of a static token. The agent uses whatever
authentication mechanism the kubeconfig defines. If the kubeconfig
calls an exec plugin, that binary must also be installed on the VM
running the agent and reachable in its execution environment.
Copy the resulting file to a path under /var/lib/almaforge on the
VM. The agent reads it via ALMA_KUBECONFIG (set in Step 5).
mkdir -p /var/lib/almaforge/kubeconfigscp prod.yaml /var/lib/almaforge/kubeconfigs/chmod 600 /var/lib/almaforge/kubeconfigs/prod.yamlOne agent can serve many clusters at once from a single
kubeconfig. The example above shows one cluster for clarity. To
add more, append additional clusters, users, and contexts
entries to the same kubeconfig (or merge multiple kubeconfigs
with KUBECONFIG=a.yaml:b.yaml kubectl config view --flatten),
then create one KubernetesTarget per context in Step 6. The
context name is what AlmaForge registers as the cluster name,
so make it match the target's metadata.name.
Step 5. Issue a join token and write the env file
alma create token --services=kubernetesWrite /etc/almaforge/almad.env with the proxy address, the token, and
the kubeconfig path. The systemd unit loads this file. Restrict
permissions because the token is sensitive. The example file sets:
ALMA_PROXY: theAlmaForgeProxy address.ALMA_CLIENT_ID: theclient_idprinted byalma create tokenin this step.ALMA_CLIENT_SECRET: theclient_secretprinted byalma create tokenin this step.ALMA_KUBERNETES_SERVICE=true: turns on the Kubernetes Service role.ALMA_KUBECONFIG: the kubeconfig path from Step 4.ALMA_MATCH_LABELS: only needed for the centralized-apply path in Step 6.
almad.env, a template to copy to /etc/almaforge/almad.env (then chmod 600):
# Environment file for the almad systemd unit on a VM.
# Path: /etc/almaforge/almad.env
# Permissions: chmod 600 (the join token is sensitive).
# AlmaForge Proxy address (host:port). The agent connects here
# over mTLS and registers with the Auth Service through the same
# socket.
ALMA_PROXY=<proxy-hostname>:443
# Join token from `alma create token --services=kubernetes --client-id=<name>`.
# Default TTL is 30 minutes. The token is NOT single-use and
# stays valid for that window. The Auth Service exchanges it
# for real credentials on first connect; identity is then
# persisted in the agent's data dir and the token is no longer
# used. Revoke a leaked token with
# `alma delete token/<client-id>`.
ALMA_CLIENT_ID=<paste-client-id-here>
ALMA_CLIENT_SECRET=<paste-client-secret-here>
# Enable the Kubernetes Service role.
ALMA_KUBERNETES_SERVICE=true
# SSH is not needed on a Kubernetes agent.
ALMA_SSH_SERVICE=false
# Path to the kubeconfig staged in Step 1. The agent uses this
# identity to impersonate AlmaForge users against the target
# cluster's API server.
ALMA_KUBECONFIG=/var/lib/almaforge/kubeconfigs/prod.yaml
# Required only when KubernetesTarget resources are applied
# centrally (Step 3). Drop this line if you use the drop-in path.
ALMA_MATCH_LABELS=env=<env>
chmod 600 /etc/almaforge/almad.envStep 6. Register the cluster
For VMs, the simplest default is centralized apply. Apply
the KubernetesTarget to the AlmaForge Auth Service from any
host with alma admin access. The resource lives in
the Auth Service backend, and any agent whose ALMA_MATCH_LABELS
selects the target's labels picks it up.
ALMA_MATCH_LABELS is the selector the agent uses for centrally
applied KubernetesTarget, AppTarget, and DatabaseTarget
resources. When one agent serves several resource kinds, the same
selector applies to all of them.
kubernetestarget.yaml, applied via alma apply:
# Registers a remote Kubernetes cluster with AlmaForge. Apply
# centrally with `alma apply -f` (recommended for VMs)
# or drop into /etc/almaforge/almad.d/ on the agent host. The shape is
# identical to the in-cluster mode.
#
# - metadata.name is the cluster name users type into
# `alma kube login`.
# - metadata.labels are matched by AlmaForge role kubernetesLabels
# and by ALMA_MATCH_LABELS on agents that pick up centrally
# applied targets.
# - spec: {} means "use the kubeconfig at ALMA_KUBECONFIG on the
# agent host".
apiVersion: almaforge.com/v1
kind: KubernetesTarget
metadata:
name: prod-us-east
labels:
env: prod
region: us-east-1
spec: {}
alma apply -f kubernetestarget.yamlkubernetestarget "<cluster-name>" has been createdAlternatively, place the same YAML in /etc/almaforge/almad.d on the remote-agent
VM. The agent reads these targets at startup. Restart it after changing
a file-mounted KubernetesTarget.
Use this path when configuration management owns the resource file
and the VM is the source of truth. Remove ALMA_MATCH_LABELS from
almad.env in this setup. The selector applies only to centrally managed
resources.
Step 7. Start the agent and verify
Install almad and its systemd unit on the remote-agent VM
before starting the service. See
Install the package.
systemctl enable --now almadalma get kubeCluster Labels Version-------------- ---------------------------- -------prod-eu-central env=prod,region=eu-central 2026.1.26The cluster can take a short time to appear. Centrally applied target
changes are watched automatically. File-mounted target changes require
systemctl restart almad. Registration is complete when
alma get kube lists it.
RBAC
This section covers the access-control chain: define an
AlmaForge Role, map it to Kubernetes groups, then bind those
groups on the cluster.
Minimal happy path:
- Create one AlmaForge role that grants access to one labeled cluster and impersonates one Kubernetes group.
- Bind that Kubernetes group to a native
ClusterRoleorRoleBindingon the cluster. - Assign the AlmaForge role to a user or SSO group.
- Log in with
alma login, thenalma kube login <cluster-name>, and runkubectl.
Overview
Two kinds of RBAC are in play. Agent RBAC is the ServiceAccount and ClusterRole that
let almad itself talk to the API server, as configured in
Step 3 of the in-cluster agent setup.
User RBAC is everything in this section: the AlmaForge
Role resources that decide which Kubernetes identity each
operator gets, plus the matching native bindings that grant that
identity real verbs.
This section covers user RBAC. It assumes agent RBAC is already in place from the deployment step.
Access to a registered cluster is governed by a two-layer model:
- AlmaForge decides which Kubernetes identity the user
gets. A AlmaForge
Rolenames one or more Kubernetes groups and users. The AlmaForge Kubernetes Service forwards eachkubectlrequest with impersonation headers that assume those groups and that user. - Kubernetes decides what that identity can do. A native
RoleBindingorClusterRoleBindingmust bind the impersonated group to aClusterRolethat grants real verbs on real resources. AlmaForge never grants Kubernetes verbs directly. It only controls who impersonates whom.
If either layer is missing, the user gets a 403.
"Impersonation" is a built-in Kubernetes mechanism. A request
that includes Impersonate-User or Impersonate-Group HTTP
headers is processed as if it came from the named user or
group, not from whoever actually authenticated the connection.
The Kubernetes API server runs the impersonated identity
through its native RBAC. AlmaForge uses this mechanism so
that operators authenticate to AlmaForge once with SSO, but
the API server still sees standard Kubernetes principals on
every request.
The end-to-end chain:
operator → SSO IdP claims → AlmaForge connector
→ AlmaForge Role(s)
→ kubernetesGroups + kubernetesUsers
→ impersonation headers on the kubectl request
→ Kubernetes RBAC (RoleBinding / ClusterRoleBinding)
→ verb on resource
On the AlmaForge side, a Role controls:
- which clusters it makes available (
kubernetesLabels) - which objects inside those clusters are visible
(
kubernetesResources) - which Kubernetes groups and users get impersonated
(
kubernetesGroups,kubernetesUsers)
On the Kubernetes side, a binding maps each impersonated
group to real verbs. AlmaForge does not create these bindings.
Adding a new group to kubernetesGroups requires a matching
RoleBinding or ClusterRoleBinding on every target cluster.
The agent's own RBAC (the ServiceAccount and ClusterRole that
let it impersonate) was already configured in
Step 3 of the in-cluster agent setup.
For remote agents, apply the 02-rbac.yaml pattern from
Step 3 of the remote cluster setup
to each target cluster.
The agent runs a SelfSubjectAccessReview preflight at
registration and logs a loud warning if impersonate is missing.
Fix the RBAC before users notice.
Step 1. Define an AlmaForge Role
A Role resource names which Kubernetes groups and users
authenticated operators get impersonated as, and optionally
filters which clusters and which objects are visible.
kube-access.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: kube-access
spec:
allow:
kubernetesLabels:
env: prod
kubernetesResources:
- kind: pod
namespace: 'app-*'
name: '*'
verbs: ['get', 'list', 'watch']
kubernetesGroups:
- developers
kubernetesUsers:
- '{{external.email}}'
The four Kubernetes-specific fields:
kubernetesLabelspicks which clusters the role makes available. It is matched at request time against the liveKubernetesheartbeat, the resource visible inalma get kube. Supports exact, glob (prod-*), regex (^.*$), and list values. UsekubernetesLabelsExpressionfor predicate logic beyond key/value matching.kubernetesResourcesfilters which objects inside matched clusters the user can see and act on. Each entry haskind,namespace,name,verbs. Anything not listed is denied.kubernetesGroupsis the list of Kubernetes groups AlmaForge impersonates on every request. Always automatic. Values from all matching roles are unioned.kubernetesUsersis the impersonated Kubernetes user. With one value, auto-selected. With many, the operator picks one with--as. Empty falls back to the AlmaForge username.
For the full schema, supported kinds, verbs, label-expression
syntax, and trait templates ({{external.email}},
{{internal.kubernetes_users}}), see the
RBAC Reference.
Map SSO traits into Kubernetes identity
SSO claims supply the traits used to resolve Kubernetes identities. The templates let one role resolve a different identity for each user.
This example expects claims named kubernetes_users and
kubernetes_groups. If your provider uses different names, update the
templates to match.
{{internal.<name>}} and {{external.<name>}} read the same trait map.
The internal. prefix accepts only the
supported variable names. Other
names produce an unsupported-variable error. Use dot notation for a
plain identifier. Use bracket notation when the claim name contains a
hyphen, colon, slash, or another character that is invalid in an
identifier.
group-member.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: group-member
spec:
allow:
kubernetesUsers:
- '{{internal.kubernetes_users}}'
kubernetesGroups:
- '{{internal.kubernetes_groups}}'
A user whose resolved kubernetes_groups trait contains
developers and viewers will be impersonated as both Kubernetes
groups, with their kubernetes_users trait supplying the
impersonated user. No per-user role edits are needed when group
membership changes.
Impersonate a Kubernetes ServiceAccount
For automation that needs a stable Kubernetes identity (CI bots,
controllers running through AlmaForge workload identity), use
the system:serviceaccount:<namespace>:<name> form in
kubernetesUsers. The ServiceAccount and its native bindings
already exist on the cluster. The AlmaForge role decides who
gets to impersonate it.
kube-bot-access.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: kube-bot-access
spec:
allow:
kubernetesLabels:
env: prod
kubernetesResources:
- kind: deployment
namespace: 'ci-*'
name: '*'
verbs: ['get', 'list', 'patch']
kubernetesUsers:
- 'system:serviceaccount:ci-system:deploy-bot'
Step 2. Bind the impersonated groups in Kubernetes
Every Kubernetes group named in any kubernetesGroups field
must be bound to a real ClusterRole (or Role) on every
target cluster. AlmaForge does not create these bindings.
For the example above, bind developers to the built-in view
ClusterRole cluster-wide.
developers-binding.yaml, applied via kubectl apply:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: almaforge-developers
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
For a namespace-scoped binding, swap ClusterRoleBinding for
RoleBinding and add metadata.namespace.
The built-in system:masters group is already bound to
cluster-admin on every Kubernetes cluster. If a AlmaForge
role impersonates system:masters, no extra binding is needed.
Treat this as a break-glass path, not a normal design pattern.
It grants cluster-admin to anyone who receives that impersonated
group, so prefer a dedicated group bound to a narrower
ClusterRole or namespace-scoped RoleBinding.
Step 3. Assign the role to users
Map an IdP claim, such as group membership, to kube-access through
the OIDC connector's claimsToRoles. The user then signs in again
to receive credentials with the new role. See
Single Sign-On.
For on-demand role escalation, see Access Requests.
Step 4. Combine roles and apply deny rules
A user with multiple roles is evaluated as follows. AlmaForge
first selects every role whose kubernetesLabels matches the
target cluster, then unions kubernetesGroups and
kubernetesUsers from each allow block. Visible objects are
the union of every allow.kubernetesResources across matching
roles. A matching deny.kubernetesGroups or
deny.kubernetesUsers entry subtracts from the impersonation
header. A matching deny.kubernetesResources entry denies access
to that resource for the request. Deny on other role fields, for
example rules, is still a whole-request veto. This lets you
grant broad access through one role and punch specific holes
through another:
kube-viewer.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: kube-viewer
spec:
allow:
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: pod
namespace: '*'
name: '*'
verbs: ['get', 'list', 'watch']
kubernetesGroups:
- viewer
nginx-exec.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: nginx-exec
spec:
allow:
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: pod
namespace: '*'
name: 'nginx-*'
verbs: ['exec']
kubernetesGroups:
- exec-and-logs
deny-redis-exec.yaml, applied via alma apply:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: deny-redis-exec
spec:
deny:
kubernetesResources:
- kind: pod
namespace: '*'
name: 'redis-*'
verbs: ['exec']
kubernetesGroups:
- exec-and-logs
A user with all three roles can list pods everywhere as viewer,
exec into nginx-* as exec-and-logs, and hit an access denial
for kubectl exec against redis-* pods. The deny rule only
affects the matching exec target. It does not remove the user's
broader read access.
For the full evaluation order, predicate logic, and trait templates, see the Access Controls reference.
Security considerations
Narrow AlmaForge roles with broad Kubernetes groups.
When you restrict a role's kubernetesResources to a subset of
namespaces or names, you are restricting what AlmaForge shows
the user. You are not restricting what the impersonated
Kubernetes group can read upstream. List operations exploit this
gap.
Suppose a user has a role that grants pod access only in the
default namespace but impersonates system:masters.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: narrow-system-masters
spec:
allow:
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: pod
namespace: 'default'
name: '*'
verbs: ['get', 'list', 'watch']
kubernetesGroups:
- system:masters
And a second role that grants pod access cluster-wide through a narrow group:
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: broad-default-viewer
spec:
allow:
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: pod
namespace: '*'
name: '*'
verbs: ['get', 'list', 'watch']
kubernetesGroups:
- default-viewer
When the user runs kubectl get pods -A, the AlmaForge
Kubernetes Service forwards the list request to the API server as
system:masters + default-viewer, gets back every pod in every
namespace, and then filters the response to what either role's
kubernetesResources allows. Because the second role allows
namespace: "*", nothing is filtered out. The user sees every
pod in the cluster, even though the system:masters role was
supposed to be scoped to default.
The fix is to keep the narrow-scoped AlmaForge role narrow on both axes: tighten the Kubernetes group too.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: narrow-default-viewer
spec:
allow:
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: pod
namespace: 'default'
name: '*'
verbs: ['get', 'list', 'watch']
kubernetesGroups:
- default-viewer
Rule of thumb: if a role restricts namespaces in
kubernetesResources, the Kubernetes group it impersonates must
be restricted to the same namespaces by a RoleBinding. Don't
mix scoped AlmaForge filters with cluster-wide Kubernetes
groups and expect the filter to hold.
Connect to Kubernetes
The user workflow is two commands: log in to AlmaForge, then log
in to the Kubernetes cluster. After that, regular kubectl
commands keep working through the exec plugin.
Log in to AlmaForge
alma --proxy=almaforge.example.com:443 loginAuthenticates against AlmaForge through SSO
and stores short-lived certificates in the alma profile
directory for follow-up commands.
List Kubernetes clusters
alma get kubeCluster Labels Version-------------- ---------------------------- -------prod-eu-central env=prod,region=eu-central 2026.1.26staging-eu-central env=staging,region=eu-central 2026.1.26The list is filtered by your AlmaForge roles. Clusters whose
labels do not match any role's kubernetesLabels are hidden.
Connect to a single cluster
alma kube login prod-eu-centralLogged into Kubernetes cluster "prod-eu-central". Try 'kubectl version' to test the connection.This adds a kubeconfig context to your default kubeconfig
(~/.kube/config, or the path in $KUBECONFIG) and switches the
active context to it. The context uses alma as a
credential exec plugin. It supplies a valid short-lived certificate
for requests through the AlmaForge Proxy, reusing cached credentials
while they remain valid.
The alma binary must be on the operator's $PATH for
kubectl to find it as the exec plugin. If the
alma session expires mid-kubectl, the plugin
returns an authentication error and the operator must re-run
alma login.
After login, regular kubectl works as usual:
kubectl get pods -n app-apikubectl exec -it webapp-0 -- /bin/shkubectl port-forward svc/api 8080:80kubectl logs -f deployment/apiSwitch between clusters
Each alma kube login adds (or refreshes) a context and
switches the active context to it. Logging in to a different
cluster is the shortest way to switch:
alma kube login staging-eu-centralkubectl get pods # now runs against stagingEvery previously logged-in cluster also remains available as a
standard kubeconfig context. The context name format is
<almaforge-cluster>-<kubernetes-cluster>. For example, if your
AlmaForge cluster name is root and the Kubernetes cluster is
prod-eu-central, the context becomes root-prod-eu-central.
List, switch, or pin
contexts with regular kubectl commands:
kubectl config get-contextskubectl config use-context root-staging-eu-centralkubectl --context=root-prod-eu-central get nodesGenerate a kubeconfig with every cluster at once
--all logs in to every cluster the user has access to in one
shot. Each lands as its own kubeconfig context. Use it on
laptops, CI runners, or scripts that hop across many clusters
without re-prompting:
alma kube login --allDefault context names follow <almaforge-cluster>-<kube-cluster>.
Override with --set-context-name and the {{.KubeName}}
template. For example, this drops the AlmaForge-cluster prefix:
alma kube login --all --set-context-name='{{.KubeName}}'--all is mutually exclusive with --labels and --query. Use
those for a subset:
alma kube login --labels='env=prod'alma kube login --query='labels["env"] == "prod" && labels["region"] != "us-east-1"'Whether you log in to one cluster or to many, alma kube login writes or refreshes a stanza in your kubeconfig
that hands credential issuance back to alma through
the
client.authentication.k8s.io/v1beta1
credential exec plugin contract.
kubectl invokes alma kube credentials when it needs
credentials, receives a short-lived X.509 certificate, and uses it
to call the AlmaForge Proxy. The certificate's TTL is bound to
the operator's AlmaForge login. When that session expires, the
plugin returns an authentication error and the operator re-runs
alma login. There are no long-lived secrets in this
file: the CA is public and credential retrieval is delegated
to the binary.
The alma binary must remain available at the path recorded
in the kubeconfig. If you move the binary, regenerate
the kubeconfig with alma kube login so the command
field points at the new path.
Show kubeconfig stanza
kubeconfig-stanza.yaml, the reference shape written by alma kube login:
# Reference shape of the kubeconfig stanza that `alma kube login`
# writes (or refreshes) under ~/.kube/config. The exec plugin hands
# credential issuance back to `alma` on every kubectl call,
# so there are no long-lived secrets in this file: the CA is public
# and short-lived X.509 certificates are minted on demand.
apiVersion: v1
kind: Config
clusters:
- name: <proxy-cluster>
cluster:
server: https://<proxy-hostname>:443
certificate-authority-data: <base64 cluster CA>
tls-server-name: kube-almaforge-proxy-alpn.<proxy-cluster>
contexts:
- name: <proxy-cluster>-<kube-cluster>
context:
cluster: <proxy-cluster>
user: <proxy-cluster>-<kube-cluster>
extensions:
- name: almaforge.kube.name
extension: <kube-cluster>
users:
- name: <proxy-cluster>-<kube-cluster>
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: alma
args:
- kube
- credentials
- --kube-cluster=<kube-cluster>
- --proxy=<proxy-hostname>:443
Success check
Run one allowed command and one denied command. Replace app-api
with a namespace your role should allow, and keep kube-system or
another namespace your role should not allow:
kubectl get pods -n app-apikubectl get pods -n kube-systemError from server (Forbidden): pods is forbidden...The setup is working if all of the following are true:
alma get kubeshows the cluster you just registered.alma kube login <cluster-name>writes a kubeconfig context without error.- The first
kubectl get podsworks in a namespace your role allows. - The second
kubectl get podsreturnsforbiddenin a namespace your role does not allow.
Transient local proxy (no kubeconfig changes)
alma proxy kube does not touch your default
kubeconfig. Instead it:
- Starts a local TCP proxy on
127.0.0.1:<random-port>. - Writes a fresh kubeconfig file to a temporary path that points at that local address.
- Prints the path so you can
export KUBECONFIG=...and run kubectl in that shell only.
When you Ctrl+C the command, the proxy stops and the temp
kubeconfig is discarded. Your ~/.kube/config is never touched.
alma proxy kube prod-eu-centralUse it when:
- You don't want to leave state in
~/.kube/config(CI, one-off scripts, shared workstations). - A corporate HTTPS proxy interferes with the direct connection
from
kubectlto the AlmaForge Proxy. The local proxy isolates the workaround fromkubectl. - Tooling expects
kubectlto talk to a fixed local address.
You can pass cluster names, --labels, or --query to choose
which clusters to proxy. With no arguments, the local proxy
serves the clusters you have already loaded with
alma kube login.
Troubleshooting
alma get kube shows nothing
The user's roles do not match the cluster labels. Run
alma get kube -o yaml as an admin to see how the
cluster is actually labeled, then compare that output with the
role's kubernetesLabels matcher.
kubectl get pods returns forbidden
A forbidden response can come from either access-control layer. Check
that the AlmaForge role permits the resource and verb through
kubernetesResources, then check the native RoleBinding or
ClusterRoleBinding for the impersonated Kubernetes identity. See
RBAC for the complete chain.
Agent logs say impersonate is forbidden
The agent's ServiceAccount, or the identity in its kubeconfig, is
missing the impersonate verb on users, groups, or
serviceaccounts. Grant it via the ClusterRole in
RBAC.
Agent logs Failed to test the necessary Kubernetes permissions
The agent's preflight SelfSubjectAccessReview check failed. The
agent still registers the cluster, but user requests hit the same
error at runtime. Either the impersonate verb is missing, or the
create verb on authorization.k8s.io/selfsubjectaccessreviews is
missing. Fix it before users hit request-time denials.
A centrally applied KubernetesTarget never shows up in alma get kube
No agent matches the target's labels. Every centrally applied target
needs at least one agent whose ALMA_MATCH_LABELS selects it. Check
agent logs and the target's metadata.labels. Agents using only the
drop-in path do not need a match label, but they also do not pick up
centrally applied targets.
alma kube login succeeds but kubectl exec is denied
The exec verb must be listed explicitly in a matching
kubernetesResources entry. verbs: ["get", "list", "watch"] allows
kubectl get pods but not kubectl exec. Add exec, and
portforward if you use kubectl port-forward, to the role.
Agent on GKE Autopilot logs cannot impersonate "system:masters"
GKE Autopilot blocks impersonation of the built-in system:masters
group at admission, even when the agent has the impersonate verb.
Use a regular Kubernetes group bound to cluster-admin instead. In
the AlmaForge Role, replace kubernetesGroups: [system:masters]
with a group like almaforge-cluster-admin, then bind it on the
cluster.
gke-autopilot-cluster-admin.yaml, applied via kubectl apply:
# Workaround for GKE Autopilot, which blocks impersonation of the
# built-in `system:masters` group at admission. Bind a regular group
# to `cluster-admin` and reference it from the AlmaForge Role's
# kubernetesGroups instead.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: almaforge-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: almaforge-cluster-admin
apiGroup: rbac.authorization.k8s.io
Agent fails to join with no authorities for hostname
The agent's persisted identity is signed by a CA the cluster no longer trusts (typically after a CA rotation or after the AlmaForge cluster was reinstalled). The agent will not re-join automatically because it still holds a stored identity.
For the in-cluster StatefulSet, delete the affected pod's
<pod>-state Secret, mint a fresh join token, update
ALMA_CLIENT_ID and ALMA_CLIENT_SECRET in the almaforge-env
Secret, and restart the pod. The healthy replica keeps serving in
the meantime.
For an external agent, stop the agent, delete its data directory
(default /var/lib/almaforge), update its join token, and start it
again.
Operations
Break-glass access
Keep one native cluster-admin break-glass path outside AlmaForge so
you can deploy or repair AlmaForge itself, fix a bad RoleBinding,
remove a broken KubernetesTarget, or disable the agent if you lock
yourself out through impersonation RBAC.
That path should stay independently permissioned at cluster-admin. It must keep working even when AlmaForge RBAC, impersonation, or target configuration is broken.
For managed Kubernetes, that usually means the cloud provider's own credential flow:
- GKE:
gcloud container clusters get-credentials - EKS:
aws eks update-kubeconfig - AKS:
az aks get-credentials --admin - Other clusters: keep an emergency admin kubeconfig, client certificate, or other pre-positioned cluster-admin credential.
High availability
For the in-cluster deployment in this page, the example StatefulSet
already runs two replicas. That is the default high-availability
shape. Each replica gets its own stable pod name and its own persisted
identity in a <pod>-state Secret. With two replicas, Kubernetes can
restart, reschedule, or roll one pod while the other keeps serving,
assuming the cluster can schedule both replicas. For real fault
isolation, make sure the cluster can place the two replicas separately
instead of landing both on the same failure domain.
For the non-Kubernetes path, run two agents on two separate hosts.
Point both at the same target clusters and, if you use centralized
apply, give both hosts matching ALMA_MATCH_LABELS so both agents
pick up the same KubernetesTarget. This gives you a second serving
path when one host is down for maintenance or fails unexpectedly.
Replica lifecycle
Each replica joins with a short-lived join token, receives its own
long-lived host identity certificates (SSH and TLS), and stores that
identity in its <pod>-state Secret. That Secret is not application
data. It is the replica's re-creatable identity state.
You do not need to back up <pod>-state as a separate
disaster-recovery asset. If a replica loses its Secret, mint a fresh
join token, update ALMA_CLIENT_ID and ALMA_CLIENT_SECRET in the
shared almaforge-env Secret, and restart only the affected pod so
it can join again and receive a new identity. The healthy replica
keeps serving from its own <pod>-state Secret and does not need to
re-join.
The agent updates the Secret when its stored identity changes. During CA rotation it can also store temporary replacement identity and rotation state in the same Secret until the rotation finishes.
Once both replicas are healthy, revoke or delete the join token. Keep
a live join token only when you are intentionally bringing up a new
replica or recovering one whose <pod>-state Secret was lost.
Kubeconfig lifecycle
This applies only to the external-agent path. Treat a kubeconfig that
embeds the output of kubectl create token as a bootstrap or
break-glass path, not as the preferred steady state. That kubeconfig
inherits the token's server-controlled lifetime. Replace it before
expiry, or use an exec-based or cloud-IAM-backed kubeconfig so the VM
does not depend on a pasted token.
Observability
The agent exposes /healthz and /readyz on the diag port
(3000 in the StatefulSet example). Kubernetes uses them for the
liveness and readiness probes. Operators can curl them too.
For now, watch:
- StatefulSet
Readycount (alert if it stays belowreplicaslonger than your normal rollout or pod-restart envelope). alma get kubereturning the cluster (alert if absent).- Pod logs for
impersonate is forbiddenorFailed to test the necessary Kubernetes permissionslines.
Upgrades
For the in-cluster deployment, upgrade by bumping the image version in the StatefulSet and applying the change. Kubernetes handles the rolling update. Because the example runs two replicas, one pod can update while the other keeps serving. Watch the rollout and wait for both replicas to become Ready before treating the upgrade as complete. Plan that upgrade as part of the same AlmaForge release rollout, and follow the release notes for any version-specific ordering or compatibility requirements.
If the new version misbehaves, roll back with kubectl rollout undo:
kubectl -n almaforge rollout undo statefulset/almaforgekubectl -n almaforge rollout status statefulset/almaforgeFor the non-Kubernetes path, upgrade one agent host at a time. Stop the first agent, install the new version, start it again, and confirm it is serving the cluster. Then repeat on the second host. Do not upgrade both remote agents at once unless you are prepared for a temporary loss of Kubernetes access through AlmaForge. Keep the remote agents aligned with the same tested AlmaForge release unless the release notes say otherwise.
Uninstall
Delete the KubernetesTarget first so AlmaForge stops advertising the
cluster.
For the in-cluster deployment, delete the agent workload and related
objects. If you deployed the example manifests as-is, deleting the
entire almaforge namespace removes its StatefulSet, ServiceAccount,
Role, RoleBinding, ConfigMap, and Secrets. It also removes any other
workloads or data in that namespace, so check its contents first.
Delete the agent's ClusterRole and ClusterRoleBinding separately.
Those cluster-scoped objects survive namespace deletion.
For the external-agent path, remove the systemd unit or other agent process, delete the kubeconfig file from the host, and revoke or remove the credential it used. That may mean deleting a pasted ServiceAccount token, revoking cloud-backed access, or removing another pre-positioned cluster credential.
Operators can also delete stale kubeconfig contexts with kubectl config delete-context.
References
- Declarative configuration: full schema for
KubernetesTargetandKubernetes. - Environment configuration:
environment variables and the
/etc/almaforge/almad.d/file-drop. - RBAC Reference: full schema for the
Roleresource, includingkubernetesLabels,kubernetesResources,kubernetesGroups, andkubernetesUsers. - CLI reference: all
alma kube,alma apply,alma get, andalmad --kubernetes-serviceflags. - Applications access: publish in-cluster HTTP, REST API, and TCP services through the same agent.