AlmaForge Server Access Guide
Give engineers SSO-backed SSH access to Linux servers across cloud
and on-prem environments with AlmaForge. Users sign in once
through SSO and use a short-lived certificate from the cluster CA
for alma ssh connections. An agent on each host dials out to
the Proxy and brokers connections through a reverse tunnel, so hosts
stay off the public internet. This brings SSH into the same control
plane as Kubernetes, applications, and databases:
- Recorded terminals, tied to a real person. PTY output
is captured for terminal playback through
alma recording play, alongside structured JSON audit events keyed to the user's SSO identity. Interactive sessions emit session events, while non-interactive commands emitSessionExecevents with the command and outcome. Capture follows your recording policy. - SSO mapped to OS logins: keep your native tools.
Users sign in with SSO and connections use their short-lived
certificate from the cluster CA. No user
authorized_keysto rotate, no bastion key, no shared password. The nativessh,scp,rsync, and IDE Remote-SSH stack still works through an OpenSSHProxyCommand. - Just-in-time elevation for sensitive hosts. Engineers request time-bounded access to production through the CLI or web UI. Slack notifies reviewers, and PagerDuty can supply on-call context. Approval grants a role for a fixed window. The cert expires when the window ends. See Access Requests.
- SSH for CI/CD and AI agents without shared join secrets. Workload identity issues short-lived certs to pipelines, jobs, and agents with the same RBAC and recording controls. Provider-backed joins avoid shared bootstrap tokens. See Workload Identity.
- Self-hosted, no telemetry, runs in air-gapped networks. The access plane runs inside your boundary. Agent hosts dial out to the Proxy through a reverse tunnel and need no public IP or inbound connection from it. See agentless OpenSSH for hosts that use their existing SSH server.
Architecture
Server access uses three main components.
almad is the server daemon. One binary exposes every
server-side role. The roles used for SSH access are:
almad --auth-serviceruns the Auth Service. It issues short-lived certificates, stores cluster state, and is the source of truth for identities, roles, and registered hosts. It stores the cluster's persistent state.almad --proxy-serviceruns the Proxy Service, the public TLS endpoint atalmaforge.example.com:443. It handles browser sign-in and brokers SSH connections through reverse tunnels to agents. By default, SSH sessions terminate at the host-side agent.almad --ssh-serviceruns the SSH Service on each Linux host. It dials out to the Proxy, holds a reverse tunnel open, authenticates each inbound SSH connection against the cluster CA, performs the role check, opens the OS session, and writes the recording.
Roles compose freely. A small cluster can run --auth-service and
--proxy-service in one almad process. Production deployments
split roles across multiple instances and run Auth, Proxy, and SSH
services in HA mode. See How it works for
deployment shapes and the full role catalog.
alma is the CLI for users and workloads. It authenticates
interactively through SSO (alma login) or non-interactively
through workload identity. Once authenticated, alma ssh
opens a session to a host, alma scp transfers files, and
alma proxy ssh lets native OpenSSH clients route through
the Proxy with an OpenSSH ProxyCommand stanza.
A user runs alma login before connecting. That uses the SSO
flow and leaves alma with a short-lived certificate on
disk. From there, alma ssh <login>@<host> authenticates
with the user's SSH certificate. The Proxy routes the connection
through the host's reverse tunnel. The host-side agent checks access
and opens an OS session
as the requested login, attaches the recorder, and forwards traffic
in both directions. The session ends when the user disconnects.
For hosts where you cannot install almad, such as legacy
systems, hardened images, or third-party appliances, register the
existing OpenSSH server through the Proxy instead. See
OpenSSH agentless mode.
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 applyRoleand join-token resources. Theadminpreset role covers both. - A Linux host (Ubuntu 20.04+, Debian 11+, RHEL/CentOS 8+, or
equivalent) with
almadalready installed and its systemd unit available. See Install AlmaForge. - Outbound network reachability from the host to the Proxy at
almaforge.example.com:443. No inbound ports are required on the host.
The three steps below deploy the SSH Service on one host, grant a user access, and verify they can reach it. Optional configuration (host user auto-provisioning, agentless OpenSSH, auditd integration) lives in Advanced options.
Step 1. Deploy the SSH Service
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 hosts running on a supported cloud.
Pick the join method that matches where the host runs.
- Static token
- AWS IAM
- GCP
- Azure
A random string with a TTL. Useful for first install and for hosts without a supported workload-identity provider.
alma create token --services=sshDefault TTL is 30 minutes. The token is not single-use: every
host you bring up during that window can consume the same value.
Each agent trades the token for its own per-host identity, persisted
under /var/lib/almaforge. Once a host has joined, the token is no longer
used by it.
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. The command then prints both client_id
and client_secret. Set ALMA_CLIENT_ID and ALMA_CLIENT_SECRET
on the host. You can also pass --client-id on the command line.
Supply the secret through ALMA_CLIENT_SECRET, not a command argument.
--services=ssh already covers the SSH Service. To also serve
applications or databases from the same almad process on
this host, use the comma-separated form
(alma create token --services=ssh,app,database) and set the
matching ALMA_*_SERVICE toggle for each role.
The agent proves identity via the host's IAM role: an EC2 instance profile or an assumed role on a self-managed host. Attach the role before applying the token.
aws-iam.yaml, applied via alma apply:
# Token for SSH Service agents that join the cluster with AWS IAM.
# Pin awsAccount and awsARN to the exact role the host assumes.
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: ssh-node
spec:
roles:
- system:ssh # Token is only valid for SSH Service hosts.
# Presence of the aws block selects the "aws" join method, which
# verifies the host via the AWS IAM caller identity.
aws:
allow:
- awsAccount: '123456789012' # AWS account that owns the host identity.
awsARN: 'arn:aws:sts::123456789012:assumed-role/almaforge-node/*' # Assumed-role ARN. Wildcards widen the trust boundary.
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 agent proves identity via the GCE metadata server. Bind a service account to the VM before applying the token.
gcp.yaml, applied via alma apply:
# Token for SSH Service agents that join with GCE metadata attestation.
# Pin projectIDs and serviceAccounts to the exact host identity.
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: ssh-node
spec:
roles:
- system:ssh # Token is only valid for SSH Service hosts.
# Presence of the gcp block selects the "gcp" join method, which
# verifies the host via GCE metadata-server attestation.
gcp:
allow:
- projectIDs:
- 'my-project' # GCP project hosting the agent.
serviceAccounts:
- 'almaforge-node@my-project.iam.gserviceaccount.com' # Service account the VM runs as.
Set ALMA_CLIENT_ID to the token name.
The agent proves identity via its Azure managed identity. Assign the managed identity to the VM before applying the token.
azure.yaml, applied via alma apply:
# Token for SSH Service agents that join with Azure managed identity.
# The allow block restricts joins to one subscription and resource group.
apiVersion: almaforge.com/v1
kind: Token
metadata:
name: ssh-node
spec:
roles:
- system:ssh # Token is only valid for SSH Service hosts.
# Presence of the azure block selects the "azure" join method, which
# verifies the host via Azure managed-identity attestation.
azure:
allow:
- subscription: '11111111-1111-1111-1111-111111111111' # Azure subscription containing the VM.
resourceGroups:
- 'servers-rg' # Restrict joins to this resource group.
Set ALMA_CLIENT_ID to the token name.
Save the Proxy address, join client ID, client secret, SSH Service
setting, and static labels in /etc/almaforge/almad.env. The systemd unit reads
this file when almad starts. The file contains the client
secret, so restrict access to root.
With ALMA_PROXY set, the agent joins through the Proxy and validates
the Proxy's TLS certificate against the host's system trust store. Use a
publicly trusted Proxy certificate or install its signing CA in the
host's trust store before the join. This check protects the first
connection, before the agent receives cluster keys.
Use ALMA_PROXY for the SSH agent in this guide. ALMA_CA_PIN applies
to direct Auth joins, not Proxy joins, and ALMA_AUTH_SERVER cannot be
set alongside ALMA_PROXY. See Hardening for
the cluster's join trust configuration.
almad.env, a template to copy to /etc/almaforge/almad.env:
# Environment file for the almad systemd unit.
#
# Path: /etc/almaforge/almad.env
# Permissions: chmod 600 (the join token is sensitive)
# Proxy address (host:port).
ALMA_PROXY=almaforge.example.com:443
# Join token from `alma create token --services=ssh --client-id=<name>`.
# Default TTL: 30 minutes. The token is NOT single-use and stays
# valid for that window. After first connect, identity is persisted
# under /var/lib/almaforge 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
# A proxy join verifies the Proxy certificate against this host's
# system trust store. Use a publicly trusted certificate or install its
# signing CA in that store. ALMA_CA_PIN pins the Auth Service CA for a
# direct join selected by ALMA_AUTH_SERVER. If ALMA_PROXY is also set,
# the Proxy takes precedence and ALMA_CA_PIN is ignored.
# Enable the SSH Service role.
ALMA_SSH_SERVICE=true
# ALMA_LABELS sets operator-defined labels as comma-separated key=value
# pairs. They appear in audit events and `alma get nodes`. Use stable
# keys such as env, region, and team. Keep fast-changing values out of
# this setting. Render inventory labels into this value when you
# provision the host.
#
# The agent also advertises generated labels. It refreshes AWS and Azure
# tags from instance metadata once an hour and prefixes their keys
# with aws/ or azure/. Hetzner supplies hetzner/instance-id,
# hetzner/region, hetzner/availability-zone, hetzner/hostname, and
# hetzner/local-ipv4. Every host that sends heartbeats also advertises
# internal/expires. Role serverLabels matchers evaluate the full set.
ALMA_LABELS=env=prod,region=eu-central-1,team=sre
# Optional short identifier for this node (defaults to OS hostname).
# Must be unique within the cluster.
# ALMA_NODE_NAME=web-01
chmod 600 /etc/almaforge/almad.envsystemctl enable --now almadALMA_LABELS sets operator-defined labels. Use stable keys such as
env, region, and team, with values up to 63 characters of letters,
digits, -, _ and ., as in env=production. Keep fast-changing
values out of this setting. If labels come from an inventory system,
render them into ALMA_LABELS when you provision the host.
The agent also advertises generated labels. On supported cloud
instances, it refreshes labels from the instance metadata service once
an hour. AWS and Azure instance tags appear with the
aws/ or azure/ prefix, rewritten to the label character set: the AWS
tag aws:cloudformation:stack-id arrives as
aws/aws.cloudformation.stack-id. Write your matchers against the
imported key. Hetzner metadata appears as
hetzner/instance-id, hetzner/region, hetzner/availability-zone,
hetzner/hostname, and hetzner/local-ipv4. Every host that sends
heartbeats also advertises an internal/expires label containing the
registration record's expiration time.
Matchers in Role.spec.allow.serverLabels evaluate the full label set
on every connection. Imported keys work like keys from ALMA_LABELS.
Named-key matchers evaluate only those keys. The "*": "*" selector
matches every host.
Confirm the host completed the join handshake from the agent side:
journalctl -u almad -n 50 | grep -Ei 'joined|registered'Then confirm AlmaForge sees the host:
alma get nodesHost UUID Public Address Labels Version------- ------------------------------------- --------------- --------------------------------------- -------web-01 3f0a6e2c-1d4b-4f3e-9c2a-5d7e8b9c0f1a env=prod,region=eu-central-1,team=sre 2026.1.26The Public Address column is empty for tunneled hosts. The agent
holds the reverse tunnel open and the Proxy reaches it through that
tunnel. The host appears within one heartbeat (a few seconds). If
it does not, see Troubleshooting.
Step 2. Grant a user access
Two layers of authorization decide what a user does on a host.
AlmaForge picks which hosts the user can reach and which OS
logins they can assume via Role.serverLabels and Role.serverLogins.
The OS decides what each login can do once AlmaForge has
opened the session: file permissions, sudoers, capabilities. The
host's local controls take over. AlmaForge never elevates a
login past what the OS grants it.
An access denial can come from AlmaForge when no role selects the host or permits the requested login, or from the OS when that login lacks permission for a command.
Apply a Role and map an IdP group to it with the SSO connector's
claimsToRoles. See OIDC for the full schema.
serverLogins and serverLabels values support trait templates
({{external.team}}, {{external.preferred_username}},
{{internal.logins}}), so a single role can scope each user to
their own slice of the inventory based on IdP claims.
- Quick Start
- SSO templates
One role, every host, fixed OS logins. For demos, dev environments, or first-time setup. Tighten later.
# Quick-start SSH role. Use fixed OS logins on any registered host,
# then replace the wildcard label matcher before production use.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: ssh-access
spec:
allow:
serverLabels:
'*': '*' # Wildcard key and value matches every Node regardless of labels.
serverLogins: # OS user accounts the role can assume on the target host.
- ubuntu
- ec2-user
deny:
serverLogins: # Hard block on root, even if another role grants it.
- root
Map a workforce SSO group to that role:
# Map an IdP group to the SSH role through an OIDC connector.
# Replace okta with your connector name and engineers with the IdP
# group whose members should receive ssh-access.
apiVersion: almaforge.com/v1
kind: OIDCConnector
metadata:
name: okta
spec:
issuerURL: 'https://example.okta.com'
clientID: '<client-id>'
clientSecret: '<client-secret>'
claimsToRoles:
- claim: 'groups'
value: 'engineers'
roles: ['ssh-access']
SSO-templated logins, scoped by environment label. The allow lists
env: ['dev', 'staging'] so production hosts are not reachable
through this role at all. An allow must cover both the host and
login. Matching deny rules take precedence. See the
role evaluation rules.
# SSO-templated SSH role. The OS login is derived from the user's IdP
# claim, so one role can serve a whole engineering group.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: ssh-engineer
spec:
options:
maxSessionTTL: 8h # Maximum credential lifetime. Use disconnectExpiredCert to end active sessions at expiry.
allow:
serverLabels:
env: ['dev', 'staging'] # Match dev or staging hosts only. Production stays out of this role.
serverLogins:
# Use the preferred_username claim supplied by the OIDC provider.
- '{{external.preferred_username}}'
Apply each YAML file from the tab you used:
# Quick Startalma apply -f ssh-basic.yaml -f ssh-sso-claims.yaml
# SSO templatesalma apply -f ssh-sso.yamlFor the full Role schema, supported matchers, predicate syntax
(serverLabelsExpression), and the complete trait-template surface,
see the RBAC reference. For on-demand
role escalation, see
Access Requests.
Security note. A deny.serverLogins: [root] blocks the root login
on every host the user can otherwise reach. It does not stop the
user from running sudo -i once they are signed in as a permitted
login. Block privilege escalation in OS sudoers, not in
the AlmaForge role.
Step 3. Verify access
After Step 1 registered the host and Step 2 gave the user a role that selects it, three entry paths reach the same SSH session:
alma ssh: the native AlmaForge client. Best for daily operator use, fan-out, file transfer, and port forwarding.- Native
sshthrough the Proxy: the standard OpenSSH client routed viaalma proxy sshasProxyCommand. Best for IDEs,rsync,git+ssh, and anything that expects anssh_config. - Web terminal: browser shell at the Proxy URL. Best for
ad-hoc access from a laptop without
almainstalled.
All three honor the same role checks. If the user is rejected, the
failure is at one of the two layers from Step 2. Proxy hides the
host when no role's serverLabels selects it. The agent denies the
login when the role's serverLogins does not include it. See
Troubleshooting for the common causes.
Before using either CLI path, sign in:
alma --proxy=almaforge.example.com:443 loginThis authenticates against AlmaForge through SSO
and writes a short-lived certificate to the alma profile
directory. Browser sessions handle their own login through the same
SSO flow.
Path 1: alma ssh
List the hosts the user can reach. The list is filtered by role:
hosts whose labels do not match any role's serverLabels are hidden.
alma get nodesHost UUID Public Address Labels Version------- ------------------------------------- --------------- --------------------------------------- -------web-01 3f0a6e2c-1d4b-4f3e-9c2a-5d7e8b9c0f1a env=prod,region=eu-central-1,team=sre 2026.1.26web-02 9b1f4d7a-2e6c-4a8b-b3d5-1e0f7c9a8b2d env=prod,region=eu-central-1,team=sre 2026.1.26db-01 2c8e7f1d-5b4a-4c9e-9f1d-7a2b6c0e5d8f env=prod,region=eu-central-1,team=dba 2026.1.26Open an interactive shell, or run a one-shot command. The OS login
(ubuntu here) must appear in the user's role serverLogins list. See
Step 2. A login the role does not
include is rejected at the Proxy.
alma ssh ubuntu@web-01alma ssh ubuntu@web-01 'systemctl status nginx'<login>@<label-expr> fans the command out across every matched
host and prints results inline. This is the fastest way to answer
fleet-wide questions without writing a playbook for a one-off:
alma ssh ubuntu@env=prod 'uptime'alma ssh ubuntu@env=prod,team=sre 'systemctl is-active nginx'Transfer files with alma scp, or point rsync at
alma ssh:
alma scp report.txt ubuntu@web-01:/tmp/rsync -avz -e "alma ssh" ./build/ ubuntu@web-01:/srv/app/Forward ports with the usual SSH flags (-L, -R, -D):
alma ssh -L 5432:localhost:5432 ubuntu@db-01Unless a role sets sshFileCopy: true or portForwarding: true, the
Proxy refuses the operation with a clear error.
Path 2: Native ssh for IDEs, rsync, git+ssh, Ansible
Tools that expect a standard OpenSSH config, such as VS Code
Remote-SSH, JetBrains Gateway, rsync, git+ssh, Ansible, and
configuration management tools, work through the Proxy with one
ProxyCommand stanza. After alma login, connect with the
synthesized hostname:
ssh ubuntu@web-01.almaforge.example.comThe OpenSSH stanza has this shape:
Host *.almaforge.example.com almaforge.example.com
UserKnownHostsFile ~/.config/almaforge/known_hosts
IdentityFile ~/.config/almaforge/profiles/almaforge.example.com/<sso-user>
CertificateFile ~/.config/almaforge/profiles/almaforge.example.com/<sso-user>-ssh/almaforge.example.com-cert.pub
HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com
Host *.almaforge.example.com !almaforge.example.com
Port 3022
ProxyCommand alma proxy ssh --cluster=almaforge.example.com --proxy=almaforge.example.com:443 %r@%h:%p
The Host *.almaforge.example.com block wildcard-matches registered nodes
and routes each match through alma proxy ssh. OpenSSH uses
the short-lived key, certificate, and known-hosts file written by
alma login. There is no DNS A record for web-01. The
host name is <node-name>.<cluster-name>, synthesized by the Proxy
from the registered node and the cluster's public address. It works
the same way for scp, sftp, rsync, and IDEs that shell out to
OpenSSH.
The cert's TTL is bound to the operator's AlmaForge login.
When that session expires, ssh returns an authentication error
and the operator re-runs alma login.
Path 3: Web terminal
Open https://almaforge.example.com in a browser, sign in through SSO,
and pick the host from the Servers list. The web terminal
opens a full PTY in the browser, recorded the same way as a
alma session.
This path needs no client install and no local cert. It is the fastest way to give a one-time approver, an auditor, or an on-call engineer a working shell from a fresh device.
Recordings from every path land in the cluster's configured session
storage backend (local disk, S3, or GCS) and are owned by the
cluster with no third party. Replay with
alma recording play or from the web UI. See
Storage Configuration for backends and
retention controls.
Server access is working when the host appears in
alma get nodes, the user's role selects it, and at least
one of the three entry paths opens a recorded shell as an allowed OS
login.
Advanced options
The subsections below are optional features. Reach for them when you need a deployment shape the basic flow does not cover (legacy OpenSSH hosts), when role behavior needs more than label-and-login control (host user auto-provisioning, role options), or when the host needs to integrate with the Linux audit subsystem (auditd).
OpenSSH agentless mode
Use this mode for hosts that already run sshd and where you cannot
install almad (legacy systems, hardened images, third-party
appliances). The Proxy brokers connections directly to sshd using
short-lived certificates from the AlmaForge CA. No long-running
agent is added to the host.
Tradeoffs vs. the SSH Service path:
- Host labels are fixed by the registration record. The host cannot refresh them through an installed agent.
- No
auditdevent injection on the host side. - The host's own
sshdpolicy applies (PermitRootLogin,AllowUsers, etc.) on top of the AlmaForge role check.
If you have a choice, prefer the SSH Service. The agentless path is for hosts you genuinely cannot run an agent on.
The host needs OpenSSH sshd 7.4 or newer, with port 22 reachable
from the AlmaForge Proxy.
Issue a node-typed token from your workstation:
alma create token --services=sshCopy the almad binary to the host, then run the agentless join
as root. The command updates the OpenSSH configuration to trust the
AlmaForge CA, registers the host with the cluster, and writes the host
identity under /var/lib/almaforge.
Keep the client secret out of command arguments. Arguments can appear
in ps output and shell history. almad join openssh reads
the secret only from ALMA_CLIENT_SECRET.
Create a root-owned file for the secret, set its mode to 0600, and
use sudoedit to enter the secret without opening a root shell:
sudo install -o root -g root -m 0600 /dev/null /root/alma-client-secretsudoedit /root/alma-client-secretPass the absolute path directly to sudo as an environment variable.
The join restarts OpenSSH by default, using the systemd unit the host
installs: sshd.service on Red Hat-family distributions and
ssh.service on Debian-family ones. To schedule the restart yourself,
add --no-restart-sshd to the command below.
sudo ALMA_CLIENT_SECRET=/root/alma-client-secret \ almad join openssh \ --address=<openssh-host>:22 \ --proxy=almaforge.example.com:443 \ --client-id=<client-id> \ --join-method=static \ --labels=env=legacy,team=opsCredentials created by alma create token use the static
join method. Place ALMA_CLIENT_SECRET after sudo, as shown. The
default sudo configuration discards variables exported before the
command.
When ALMA_CLIENT_SECRET contains an absolute path, the command reads
the file and trims its contents. Any other value, including a relative
path, is treated as the secret itself.
The join runs once and exits. After it succeeds, the host has its own
signed identity on disk. Delete /root/alma-client-secret when you no
longer need it.
Confirm the join from your workstation:
alma get nodesThe agentless host has an empty Version column because no
AlmaForge process runs there to report a version. Connect to it as
you would an SSH Service host:
alma ssh <login>@<openssh-host>The join prepends one line to /etc/ssh/sshd_config:
Include /var/lib/almaforge/sshd.conf.
The rest of /etc/ssh/sshd_config remains unchanged. The included file
points TrustedUserCAKeys, HostKey, and HostCertificate to paths
under /var/lib/almaforge/openssh. The join stores the corresponding key
material in that directory.
To remove the host, delete the prepended Include line and reload
sshd. Then delete /var/lib/almaforge/sshd.conf and /var/lib/almaforge/openssh.
Finally, remove the node record from your workstation:
alma delete node/<host-id>Auto-create host users
By default, the OS login named in Role.serverLogins must already exist
on the host. To skip that pre-provisioning step, enable host user
auto-provisioning. The agent creates the OS account at session
start, adds it to the listed hostGroups, writes any
hostSudoers entries to /etc/sudoers.d, and (with
createHostUserMode: drop) deletes the account at session end.
# SSH role with host user auto-provisioning. The agent creates the OS
# account at session start and, with mode=drop, deletes it at session
# end so you do not pre-create per-engineer logins on the host.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: ssh-auto-users
spec:
options:
# drop: create at session start, delete at session end (transient).
# keep: create at session start, leave the account in place after.
# off: do not auto-create. The login must already exist.
createHostUserMode: drop
allow:
serverLabels:
env: 'dev'
serverLogins:
- '{{external.preferred_username}}' # Per-user OS account, named from the IdP claim.
hostGroups: # Groups the auto-created user is added to. Missing groups are created.
- docker
- developers
hostSudoers: # Sudoers entries written under /etc/sudoers.d for the session.
- 'ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx.service'
createHostUserMode accepts:
drop: create at session start, delete after the user's last session ends. Account cleanup does not undo changes made during a session.keep: create at session start, leave the account in place after.off: disable auto-creation (the default). The OS login must already exist.
Every role matching the target server has to opt in. An unset
createHostUserMode counts as off, so one matching role that omits
the field blocks account creation for the whole session. The built-in
access role matches every server and omits it, so a user who holds it
alongside the role above sees unknown user <login>. Scope the roles
so only one matches, or set createHostUserMode on every matching
role.
The agent invokes useradd, userdel, usermod, groupadd,
getent, and visudo to manage users, so it must run as root on
the host. The packaged systemd unit already does. Those binaries
must be on the agent's $PATH. Each value of Role.serverLogins must
conform to the host's username rules (see
User/Group Name Syntax).
hostSudoers is a session-scoped privilege grant. Entries are
written to /etc/sudoers.d for the duration of the session and
removed at session end. They survive an agent crash and are cleaned
up on next agent start. If you mint a sudoers rule that grants
ALL=(root) NOPASSWD: ALL, you have given the user full root for
that session. The cleanup at session end does not undo what they
did with it.
Role options for SSH
Role options apply per-session and override the cluster default. The fields below cover the controls operators tune most often. For the full list, see the RBAC reference.
# Reference SSH role exercising the role options operators tune
# most often. Trim to taste. Every option below has a sensible
# default, so this file is meant to be read field-by-field.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: ssh-options-ref
spec:
options:
maxSessionTTL: 8h # Maximum credential lifetime.
forwardAgent: true # Allow native SSH agent forwarding through the Proxy.
portForwarding: true # Allow `-L`/`-R` TCP forwards.
sshFileCopy: true # Allow SCP and SFTP. Omit to block file copy.
clientIdleTimeout: 30m # Disconnect idle clients after this period. "never" disables.
disconnectExpiredCert: true # End the session if the user's cert expires mid-session.
maxSessions: 10 # Max channels per SSH connection. 10 matches OpenSSH.
maxConnections: 0 # Cluster-wide concurrent session cap. 0 = no limit. Adds backend load when set.
pinSourceIp: false # Embed the login IP in the cert. The cert refuses use from any other IP.
recordSession:
default: bestEffort # Fallback recording mode for protocols without an explicit setting.
ssh: bestEffort # Per-protocol mode for SSH sessions.
allow:
serverLabels:
env: ['dev', 'staging']
serverLogins:
- '{{external.preferred_username}}'
Notable behaviors:
portForwardingandsshFileCopygrantssh -L/-Rand SCP/SFTP. Neither is granted unless a role sets it totrue, so a role that omits them allows interactive sessions only.pinSourceIp: truebinds the certificate to the operator's sign-in IP and refuses use from any other IP. Breaks if the IP changes mid-session (mobile networks, VPN reconnect).recordSession.ssh: strictterminates the session if the recorder cannot write: full disk, lost connection to the storage backend. Use it on prod hosts where an unrecorded session is unacceptable.bestEffortkeeps the session running and drops the recording.maxSessionTTLcaps the lifetime of certificates issued for this role (default12h). Lower it for sensitive prod roles so a compromised cert expires sooner. Raise it for long-running workload-identity bots.
auditd integration
When the host runs the Linux Auditing System (auditd) and the SSH
Service has the privileges to talk to it, AlmaForge emits
structured events into the audit log automatically. No
configuration is needed. The events carry the AlmaForge username,
role set, and session ID, so a downstream auditd rule can
correlate kernel-level events with the AlmaForge audit trail and
SIEM pipelines pick up SSH activity alongside everything else
auditd captures.
Identity files for non-interactive SSH
Scripts and CI jobs that should not run a browser-based SSO flow use
an identity file, a single file holding a short-lived
certificate plus the private key. An operator generates one once on
their workstation by adding -o to the regular SSO login flow, then
hands the file to the job. The job points alma at it
through the global -i flag:
# On the operator's workstation, after a normal SSO login:alma login --proxy=almaforge.example.com:443 -f file -o ./ci-identity.pem
# In the CI job:alma -i ./ci-identity.pem ssh ubuntu@web-01 'deploy.sh'The file inherits the cert TTL and roles of the operator who
generated it (capped per role by maxSessionTTL, which is
12h unless the role raises it).
Treat it like any other secret: store it in a CI secret manager,
mount it read-only into the job, and rotate before expiry.
For long-running automation, prefer the Workload Identity guide. It issues certs continuously without a human bootstrap step.
Login banner
Set a cluster-wide login banner, such as legal disclaimers,
change-freeze notices, and ownership reminders, through the
cluster's AuthConfig resource. The banner is displayed on
alma login and in the web UI before the user reaches the
dashboard. It does not appear on each alma ssh session.
For per-host banners, use sshd's Banner directive on the host
itself.
# Cluster-wide login banner shown during user login.
# Apply this when every user must see the same legal or ownership notice.
apiVersion: almaforge.com/v1
kind: AuthConfig
metadata:
name: cluster # AuthConfig is cluster-wide. The required name is cluster.
spec:
messageOfTheDay: |
Authorized access only. All sessions are recorded.
See policy at https://wiki.example.com/access.
Apply with alma apply -f login-banner.yaml. Clear the
banner by setting messageOfTheDay: "" and re-applying.
Troubleshooting
alma get nodes shows nothing
The user's roles do not match any host's labels. Run
alma get nodes -o yaml as an admin to see how each host
is actually labeled, then compare that output with the role's
serverLabels matcher. If the admin also sees an empty list, no
agent has joined yet. Check journalctl -u almad on the
host for the join-handshake error.
alma ssh is denied for the requested OS login
The Proxy returns access denied to <user> (where <user> is the
requested OS login) when the role's serverLogins list does not include
that user. Check alma get role/<role-name> -o yaml and
confirm the expanded serverLogins value (after trait templating)
contains the user the operator typed.
Token rejected with "only valid for the X join method"
The Token's method block does not match the proof the host presents.
alma create token creates static join credentials. For a
platform-native join, apply a Token with the matching method block
and set ALMA_JOIN_METHOD on the host. --services=ssh selects the
service, not the proof method. Inspect
alma get token/<client-id> -o yaml and compare it with the
host's join environment. See the
Token schema.
Session disconnects with "client certificate expired"
When disconnectExpiredCert is enabled, an active session disconnects
when the user's certificate expires. The TTL is bounded by
maxSessionTTL on the role and the --ttl flag on
alma login. Re-run alma login
and reconnect. For long-running roles (CI bots, daemons), raise
maxSessionTTL on the role or switch the workload to
Workload Identity.
Renewed credentials apply to new connections, not an already-open
SSH session. See role options.
ssh denied with "node does not allow port forwarding"
Returned when a user runs ssh -L, -R, or -D and no role they
hold sets portForwarding: true. The interactive shell still works.
Only the forward is denied. Either set portForwarding: true on a
role the user has, or use a different role for forward-heavy
workflows.
alma ssh denied with "lock in force"
The user, the role, the device, or the host has an active Lock
resource attached, blocking new sessions. Locks are how operators
short-circuit access without touching the role definition. List
active locks with alma get locks and remove the
matching one with alma delete lock/<name>.
alma ssh rejected with "too many session channels"
The connection reached the role's maxSessions limit for concurrent
SSH session channels. Close unused channels or review that limit.
When the user holds several roles, the smallest nonzero limit applies.
alma ssh succeeds but the session hangs
The agent accepted the connection but cannot start a PTY. Common causes:
- Host user auto-provisioning is enabled but
useraddis missing or the agent does not have permission to invoke it. - The host's
/etc/security/limits.confrejects the new session (process or file-descriptor limit reached). - A failing
recordSession.ssh: strictsetting plus a full disk on the recording backend. The session is held until the recording write times out.
Check journalctl -u almad on the host for the specific
error.
Failed to launch: fork/exec /proc/self/exe: permission denied
The almad binary lacks read+execute for the OS user the
session is opening as. Some hardened images strip world-execute on
freshly installed binaries. Restore it:
sudo chmod go+rx $(which almad)sudo systemctl restart almadA node is still listed minutes after the host shut down
alma get nodes reflects the last known heartbeat. A host
that stops sending heartbeats remains visible until its heartbeat
expires (10 minutes by default). This is heartbeat lag, not
a configuration problem. To force-remove a host you have already
decommissioned:
alma delete node/<host-id>To distinguish a transient gap from a real shutdown, check the
expires field on the resource:
alma get nodes -o json \ | jq '.[] | {hostname: .spec.hostname, expires: .metadata.labels["internal/expires"]}'If the time-to-expire is closer than the normal heartbeat window (under two or three minutes), the host has stopped heartbeating.
Auto-created host user is not removed at session end
createHostUserMode: keep is the most common cause. It is the
documented behavior. If the role specifies drop and the user
still persists, check journalctl -u almad for a
userdel failure. Active processes owned by the user, an open
file in their home directory, or userdel not being on the agent
PATH all block cleanup.
OpenSSH agentless host fails with "no authorities for hostname"
The host's sshd_config no longer trusts the active AlmaForge
CA, typically after a CA rotation or after the cluster was
reinstalled. Re-run almad join openssh on the host with a
fresh token to refresh the trusted CA list, then restart sshd.
OpenSSH agentless host refuses login as root
alma ssh root@<openssh-host> returns the host's own
permission-denied error, distinct from the AlmaForge Proxy denial
in the login-denied entry above.
The agentless mode hands off straight to sshd, which enforces its
own root-login policy. If /etc/ssh/sshd_config sets
PermitRootLogin no or PermitRootLogin forced-commands-only,
sshd rejects the session before any AlmaForge layer sees it.
Either pick a non-root login, or change the setting to
PermitRootLogin prohibit-password (which still blocks password
auth and only allows certificate-based root login) and reload
sshd. SSH Service hosts are not affected.
almad --ssh-service opens the OS session itself and does not
consult sshd_config.
References
- Declarative configuration: full
schema for the
Noderesource and every other resource the SSH Service reads. - Environment configuration: every
ALMA_*environment variable the SSH Service reads at startup, includingALMA_SSH_SERVICEandALMA_LABELS. - RBAC reference: full schema for the
Roleresource, includingserverLabels,serverLogins,hostGroups,hostSudoers,createHostUserMode, and theoptionsblock. - CLI reference: all
alma ssh,alma scp,alma proxy ssh,alma apply,alma get, andalmad --ssh-serviceflags. - Audit events: the record shape and the
fields carried by the
SessionStart,SessionExec,SessionPortForward, andSessionEndevents emitted for every SSH session. - Storage Configuration: backend choices for session recordings and audit events (local disk, S3, GCS, Azure Blob).
- Agent topology: how many agent processes to run per resource pool, and how they join the cluster.
- Break-glass access: recovery paths when SSO or the cluster is unreachable.
- Agent fleet upgrades: when to upgrade agents relative to the cluster, and how to verify each one.
- Access Requests: on-demand role escalation for time-bounded production access.