SSH, sudo, and parallel apply
Richt uses the system OpenSSH ssh executable. Existing ssh_config aliases,
identity files, SSH agent, proxy/jump settings, and known-hosts policy remain in
effect. The controller needs OpenSSH in PATH; remote targets need the Linux
utilities used by the selected resources (GNU coreutils/findutils, getent,
and, when requested, DNF 4/YUM 4, systemd, or SELinux tools). No Richt daemon or
installed Richt binary is needed on an SSH target.
tags:
application:
services:
application: {}
services:
application: {order: 10, file: services/application.yaml}
hosts:
- name: application-a
tags: [application]
transport:
type: ssh
host: app-a # address or existing ssh_config alias
user: deploy # optional; otherwise use SSH configuration
port: 2222 # optional; otherwise use SSH configuration
sudo: true # explicit; never enabled implicitly
host accepts a hostname, address, or SSH alias; use the separate user field
instead of user@host or an SSH URI. Explicit host, user, and port select
the destination and override client
defaults. SSH configuration is read on every invocation. Saved plans keep these
explicit inputs and the sudo flag, not a snapshot of resolved SSH options,
keys, or passwords. Changes to ssh_config between diff and apply are allowed,
including changes to the address behind an alias. Review the alias, explicit
overrides, and privilege mode displayed before applying a plan.
SSH always runs in batch mode without a terminal. Richt does not prompt for login passwords, key passphrases, or host-key confirmation. Configure keys or load them into your agent beforehand, and establish host trust using your normal SSH workflow. The normal client host-key policy remains effective; Richt never disables verification. Authentication or trust failures fail the affected host. Client settings that replace the requested command, disable its session, or discard its stdin are overridden so the prepared operation can execute.
When sudo: true, target commands and file operations use sudo -n. Configure
noninteractive privilege access for the required commands before preparing the
plan. A password-required sudo policy fails rather than opening a prompt.
sudo: false leaves the login user’s privileges unchanged. Local transport also
accepts explicit sudo. apply -y skips only Richt’s plan confirmation; it does
not enable interactive authentication or change privileges.
Files are read on the target during diff. Prepared content is sent through SSH stdin during apply, including for privileged writes; file content is never put into command arguments. Writes follow ordinary target and parent symlinks, and removal unlinks the named file or symlink. Owner/group names are resolved on the target at observation and again when attributes are applied. Changed numeric IDs are allowed. These rules also apply to local files. The saved file bytes, resource requests, task order, and process branches remain fixed; apply does not reread inventory/templates or recalculate changes.
YAML secret sources are read on the controller during preparation, including
when writes use SSH or sudo. Apply and continuation use real saved bytes without
reading those sources. Secret calls automatically hide the entire old/new file
diff; explicit sensitive: true also protects old bytes when removing a secret
call or a file. Set target mode: "0600" explicitly when required. Saved plans
contain real secrets and use mode 0600, as do their locks and atomic replacements.
See YAML secrets and sensitive files
.
Parallel execution and continuation
./bin/richt diff -i examples/ssh/inventory.yaml -o /tmp/richt-plan.json
./bin/richt apply -y --parallelism 2 /tmp/richt-plan.json
# Continue unfinished tasks, with a different limit:
./bin/richt apply -y --parallelism 1 /tmp/richt-plan.json
Each apply handles at most 20 hosts concurrently by default. --parallelism N
sets a positive limit for that run; 1 runs sequentially. The limit is not part
of the saved plan and does not select hosts or alter tasks. Inventory/host/tag/
service filters remain forbidden when applying a saved plan. Local and SSH
hosts can share one plan.
Services run by service-definition order, and tasks remain sequential within each host. A task failure blocks the rest of its host; other active and queued hosts continue. All hosts finish before the final ordered summary, and any failure produces a nonzero exit. Explicit continuation skips successful tasks and retries unfinished tasks. There are no automatic retries or rollback.
All task progress changes and atomic plan saves are synchronized. The adjacent lock still prevents two processes applying the same saved plan. A save failure stops admission of new actions globally. Already admitted actions are awaited and may have taken effect; interrupted unfinished tasks can run again on an explicit continuation. SSH client cancellation cannot guarantee that a command already accepted by the remote host stopped.
Developer fixture acceptance
The following harness uses custom fixture RPMs for developer regressions. It is
separate from the ready-vmango walkthrough
and
current package acceptance, which do not build RPMs. For a file-only SSH
scenario, define configs with transport: {type: ssh, host: <alias>, sudo: true}
and use the same diff/save/apply workflow without a package repository. See
validation
for newly recorded runs.
Use two isolated Linux SSH targets with their own RPM database, filesystem, and
systemd instance. Both need an SSH alias, a non-root login with noninteractive
sudo, and the fixture repository produced by
build-repository.sh
at
/var/lib/richt-demo-fixture/repo. The test manages only its fixture resources,
but installs/removes packages and services: use disposable targets.
The SSH harness
records plans, command output, and
an acceptance report. It also creates and removes a unique test user/group to
verify ownership changes between preparation and apply. This stage needs the
target shadow-utils commands (useradd, usermod, groupadd, groupmod,
userdel, and groupdel). See its --help for target and output options. The local
file harness
and service harness
remain regression
checks; concurrency and persistence failures also have Go race-detector tests.
go test -race ./plan ./engine ./config ./transport ./cli
python3 tests/integration/ssh.py --binary ./bin/richt \
--host-a richt-test-a --host-b richt-test-b \
--work-root /tmp/richt-ssh-acceptance
The recorded fixture run on 2026-11-01 used two isolated AlmaLinux 10.2 x86_64 system containers, each running its own SSH server, systemd 257, filesystem, and DNF 4.20 RPM database, on a disposable SELinux-Enforcing Linux host. The 31 SSH harness checks passed with non-root logins, SSH-agent authentication, and noninteractive sudo. The 17 local file and 33 local RPM/systemd checks also passed.
The SSH run includes two independent YAML sources, automatic sensitivity, partial failure and continuation after deleting the source files, frozen multiline bytes, and mode 0600 on both targets. Secret bytes are checked by digest so reports do not contain their values. Both containers and the temporary SSH agent were stopped after verification; reports were retained.
A macOS controller also passed six secret checks through a non-root login to the enforcing Linux host with explicit sudo: controller-local sources, automatically hidden files, a deliberate partial failure after deleting sources, continuation that skips successful tasks, frozen multiline content, root-owned mode 0600 targets with the requested SELinux label, and private saved progress. Its unique temporary inputs and targets were removed after verification.
Earlier KRG-73 checks verified changed live SSH aliases, unknown/changed host-key rejection, password-required sudo failure, conflicting SSH session options, and local named UID/GID changes after preparation. A macOS controller prepared and applied a sensitive file with an explicit SELinux label on the Linux host, then observed no changes. Container isolation is not evidence of a separate kernel; SELinux file behavior was verified on the enforcing host itself.