SSH access model for Hermes Agent on Linux VMs using a single hermes account with passwordless sudo and Hermes-side approval for non-read execution.
For another Hermes instance that only needs the operational rules, start with HERMES-HANDOFF.md.
This repository also contains a Hermes skill for the local SSH client side of FreeIPA-joined Hermes instances:
skills/devops/hermes-ssh-namespace-wrapper/
Use that skill only when the Hermes runtime itself runs on a FreeIPA-joined Linux system and OpenSSH fails locally because /etc/ssh/ssh_config.d/04-ipa.conf appears as nobody:nobody inside the Hermes execution context.
Hermes should be able to administer Linux VMs over SSH without getting blocked by interactive prompts, while still preserving a clear operator workflow:
-
Read/debug actions run directly
- Read logs.
- Inspect service state.
- Inspect processes, ports, routes, disk, memory, mounts, and configuration.
- Gather evidence quickly without manual confirmation.
-
Non-read actions require user approval before execution
- Write files under privileged paths such as
/rootor/etc. - Restart, reload, enable, or disable services.
- Install, remove, or update packages.
- Change permissions, ownership, firewall state, storage state, or any other system state.
- Write files under privileged paths such as
Important constraint: do not use an interactive remote sudo password prompt as the approval model. Hermes cannot reliably operate a remote interactive sudo password prompt through tool-driven SSH sessions. The practical model is:
- one SSH identity:
hermes; - target-side sudo grants
hermesfull passwordless sudo; - Hermes approval mode decides whether a non-read command may be submitted;
- SSH, sudo, journald, auditd, and central logs provide accountability.
This setup treats Hermes as a trusted automation operator. It is not a sandbox for an untrusted model.
Because Hermes can use the configured SSH key on its own, separating hermes-read and hermes-admin identities is not a strong security boundary unless the Hermes runtime itself is prevented from accessing the admin key. If the same Hermes runtime can choose either key, two users mostly improve logging and attribution, not security.
The chosen model is therefore intentionally simple:
| Boundary | Decision |
|---|---|
| Remote Unix identity | Single hermes user |
| Remote sudo policy | hermes ALL=(ALL) NOPASSWD: ALL |
| Approval boundary | Hermes must ask before non-read/system-changing execution |
| Logging boundary | Host logs record SSH login and sudo commands by hermes |
This means target-side sudo does not prevent Hermes from changing the system. The prevention point is Hermes approval behavior and operator discipline. If Hermes is run with approval bypass / YOLO mode, this model becomes effectively unattended passwordless root over SSH.
Use one Unix account and one SSH key:
| Identity | Purpose | sudo model | Hermes approval |
|---|---|---|---|
hermes |
All diagnostics and approved administration | NOPASSWD: ALL |
Required for non-read commands |
Recommended Hermes configuration:
hermes config set approvals.mode manualsmart can be evaluated later, but start with manual for sysadmin access. Do not use YOLO mode for production systems.
A command is read/debug when it only observes system state and does not persistently change files, services, packages, users, firewall rules, kernel settings, mounts, or data.
Examples that usually count as read/debug:
journalctl, systemctl status, systemctl show, ss, ip addr, ip route,
ps, top, free, df, du, cat, tail, grep, find without -delete/-exec,
sudo -l, getent, id, ls, stat
Examples that are non-read and require approval:
systemctl restart|reload|enable|disable|start|stop
apt|apt-get|dnf|yum|apk|rpm package changes
tee/cp/mv/rm/install/chmod/chown to privileged paths
useradd/usermod/groupadd/passwd
mount/umount
firewall-cmd/iptables/nft/ufw changes
sysctl -w or writes under /proc/sys
any shell pipeline whose purpose is to modify state
flowchart TD
A[Hermes receives sysadmin task] --> B{Read/debug only?}
B -- Yes --> C[Use SSH identity: hermes]
C --> D[Run command with sudo -n when needed]
D --> E[Return evidence: logs, status, metrics, config]
B -- No, changes state --> F[Ask user for approval]
F --> G{Approved?}
G -- No --> H[Stop: do not execute]
G -- Yes --> I[Use SSH identity: hermes]
I --> J[Run approved command with sudo -n]
J --> K[Verify the result]
K --> L[Report what changed and show evidence]
D -. remote privilege .-> M[(sudoers: hermes NOPASSWD: ALL)]
J -. remote privilege .-> M
F -. approval boundary .-> N[(Hermes approvals.mode manual)]
Equivalent ASCII view:
Task received
|
v
Read/debug only? ---- yes ----> hermes SSH ----> sudo -n if needed ----> evidence
|
no
v
Ask user approval ---- no ----> stop
|
yes
v
hermes SSH ----> approved sudo command ----> verify change ----> report evidence
These examples use current FreeIPA CLI syntax and avoid an interactive sudo password model.
Run on a FreeIPA admin host with valid Kerberos credentials:
kinit admin
ipa user-add hermes \
--first=Hermes \
--last=Agent \
--email=hermes@example.com \
--shell=/bin/bash
ipa user-mod hermes \
--sshpubkey="ssh-ed25519 AAAA... hermes@example.com"Verify:
ipa user-show hermesFreeIPA HBAC must allow both:
- the
sshdHBAC service for SSH login; - the Sudo service group for sudo.
ipa hbacrule-add "hermes-access"
ipa hbacrule-add-user "hermes-access" --users=hermes
ipa hbacrule-add-service "hermes-access" --hbacsvcs=sshd
ipa hbacrule-add-service "hermes-access" --hbacsvcgroups=Sudo
ipa hbacrule-mod "hermes-access" --hostcat=allDo not use --hbacsvcs=Sudo for sudo. The sudo side must use the service group form: --hbacsvcgroups=Sudo.
Verify:
ipa hbacrule-show "hermes-access"Expected essentials:
Rule name: hermes-access
Host category: all
Users: hermes
HBAC Services: sshd
HBAC Service Groups: Sudo
Use one sudo rule granting full passwordless sudo to hermes:
ipa sudorule-add "hermes-all" \
--desc="Full passwordless sudo for Hermes Agent; non-read commands require Hermes approval" \
--hostcat=all \
--runasusercat=all \
--runasgroupcat=all \
--cmdcat=all
ipa sudorule-add-user "hermes-all" --users=hermes
ipa sudorule-add-option "hermes-all" --sudooption='!authenticate'!authenticate is the FreeIPA sudo option form for NOPASSWD.
On the FreeIPA server or admin host:
ipa sudorule-show "hermes-all"
ipa hbacrule-show "hermes-access"
ipa user-show hermesOn each enrolled target host:
getent passwd hermes
id hermes
sudo sss_cache -E
sudo systemctl restart sssd
sudo -l -U hermesExpected sudo listing should include unrestricted passwordless sudo for hermes, equivalent to:
(root) NOPASSWD: ALL
For non-FreeIPA hosts such as Alpine, Debian/Ubuntu, or RHEL-family systems, use the playbook in this repository:
ansible/deploy-hermes-ssh-access.yml
The playbook:
- creates the local
hermesuser; - installs the configured SSH public key;
- installs
sudowhere required; - writes
/etc/sudoers.d/10-hermes; - grants
hermes ALL=(ALL) NOPASSWD: ALL; - validates the sudoers file with
visudo -cf %sbefore installation; - handles Alpine, Debian/Ubuntu, and RHEL-family package installation.
Example run:
ansible-playbook \
-i inventory.ini \
ansible/deploy-hermes-ssh-access.yml \
-e hermes_ssh_public_key='ssh-ed25519 AAAA... hermes@example.com'For production, prefer Ansible Vault, inventory variables, or a secure controller-side variable source for keys instead of long command-line variables.
ssh -i ./hermes.key hermes@host.example.com 'whoami; id; hostname -f'These should work without a remote password:
ssh -i ./hermes.key hermes@host.example.com 'sudo -n whoami'
ssh -i ./hermes.key hermes@host.example.com 'sudo -n -l'
ssh -i ./hermes.key hermes@host.example.com 'sudo -n journalctl --no-pager -n 3'Expected:
root
and sudo -l should show passwordless ALL for hermes.
FreeIPA-enrolled systems normally install an OpenSSH client drop-in:
/etc/ssh/ssh_config.d/04-ipa.conf
When Hermes itself runs as a systemd user service with namespace sandboxing, root-owned host files may appear as nobody:nobody inside the Hermes tool context. OpenSSH can then fail before any remote connection is attempted:
Bad owner or permissions on /etc/ssh/ssh_config.d/04-ipa.conf
If the host/root shell shows the file as root:root but the Hermes context shows it as nobody:nobody, install the bundled Hermes skill:
# from this repository clone
mkdir -p ~/.hermes/skills/devops
cp -a skills/devops/hermes-ssh-namespace-wrapper ~/.hermes/skills/devops/
~/.hermes/skills/devops/hermes-ssh-namespace-wrapper/scripts/install-hermes-ssh-wrapper.sh --diagnose
~/.hermes/skills/devops/hermes-ssh-namespace-wrapper/scripts/install-hermes-ssh-wrapper.sh --installFor a named Hermes profile:
profile=<profile>
mkdir -p ~/.hermes/profiles/$profile/skills/devops
cp -a skills/devops/hermes-ssh-namespace-wrapper ~/.hermes/profiles/$profile/skills/devops/
~/.hermes/profiles/$profile/skills/devops/hermes-ssh-namespace-wrapper/scripts/install-hermes-ssh-wrapper.sh --diagnose
~/.hermes/profiles/$profile/skills/devops/hermes-ssh-namespace-wrapper/scripts/install-hermes-ssh-wrapper.sh --installDo not install this wrapper on non-FreeIPA systems by default. If /etc/ssh/ssh_config.d/04-ipa.conf is absent, diagnose the local SSH error separately.
No approval should be needed for read-only inspection:
ssh -i ./hermes.key hermes@host.example.com 'sudo -n systemctl status sshd --no-pager'
ssh -i ./hermes.key hermes@host.example.com 'sudo -n ss -tlnp'
ssh -i ./hermes.key hermes@host.example.com 'sudo -n df -h'Hermes should ask for approval before submitting commands like this:
ssh -i ./hermes.key hermes@host.example.com 'echo test | sudo -n tee /root/hermes-test.txt'
ssh -i ./hermes.key hermes@host.example.com 'sudo -n rm -f /root/hermes-test.txt'After approval and execution, verify the result and collect evidence from logs:
ssh -i ./hermes.key hermes@host.example.com 'sudo -n journalctl _COMM=sudo --no-pager -n 20'- Keep Hermes approval mode enabled for sysadmin work:
hermes config set approvals.mode manual. - Do not use YOLO mode for production systems with this SSH key.
- Treat the
hermesSSH key as privileged. It is effectively passwordless root on every target where this setup is installed. - Scope FreeIPA rules and SSH deployment by host groups where possible instead of using
--hostcat=allpermanently. - Rotate the
hermesSSH key according to your privileged-access policy. - Prefer explicit commands and verification: inspect first, ask for approval for non-read changes, execute, then prove the result.
- Log centrally. At minimum, collect SSH authentication logs and sudo logs. For stronger accountability, forward journald/auditd events to the central logging platform.
- Document which Hermes profile or runtime is allowed to hold and use the
hermesprivate key.
- This model is not a target-side security boundary. The target grants
hermesfull passwordless sudo. - The approval boundary exists in Hermes. If Hermes approval is bypassed or misconfigured, the remote host will not stop state-changing commands.
NOPASSWDis intentional. The alternative, an interactive sudo password prompt, is not reliable for Hermes SSH automation.- A compromised Hermes runtime or leaked
hermesprivate key can perform root actions on allowed hosts. - Separate Unix users can still be useful for logging, attribution, or key lifecycle management, but they are not a security gain if the same Hermes runtime can access all keys.