Servers (SSH)
Diagnose and fix applications on Linux servers over SSH, through a gate that runs only what each server allows. 3AM never gets a shell.
When an application on a server goes down, 3AM does what an on-call engineer would. It reads the service's status and logs, finds the error line, checks disk, memory and its port, and looks up the error in your runbooks and knowledge base. It then fixes the proven cause, with approval:
| Proven cause | What 3AM does |
|---|---|
| A crash with nothing in its logs a restart wouldn't fix | Restart it, then check it stays up |
| Out of memory | Restart it, and tell its owner to raise the heap or find the leak |
| Its disk is full | Delete its old rotated logs (only what the server allows), then restart it |
| Its database is down | Nothing on the server: the database has its own incident |
| Its database was down and is back, but it gave up | Restart it |
| Its port is taken | Advice: the process holding the port is named |
| A bad deploy | Advice: roll back. A restart would start the same broken build |
| Deadlocked | Restart it, and keep the thread dump for the developers |
The guardrails
3AM never gets a shell, and no model ever writes a command. Three separate layers each stop a command nobody allowed:
- 3AM. Hosts are on an allow-list with pinned host keys. Arguments are plain words only. Actions come only from the
server's own catalogue, and 3AM refuses anything destructive (
rm -rf,mkfs,dd,reboot,curl | sh, ...) even if a server allows it. - The server. 3AM's key may only run
3am-gate. The gate runs read-only diagnostics, plus the actions listed in/etc/3am/gate.conf, each parameter checked against its pattern. Everything else is refused and logged to syslog. - sudo. It allows exactly the privileged commands those actions need, and nothing more.
Reads change nothing and run without approval. Every action is rehearsed, approved and recorded. Secrets in logs (passwords, tokens, keys, card numbers) are masked before anything leaves the connector.
1. Install the gate on each server
As root, with the files from the bundle's ssh/ folder (3am-gate, gate.conf.example, sudoers.example); copy them to
the server first:
useradd -m -s /bin/sh threeam
usermod -aG systemd-journal threeam # read the units' journals
install -o root -g root -m 0755 3am-gate /usr/local/bin/3am-gate
install -d -o root -g root -m 0755 /etc/3am
install -o root -g root -m 0644 gate.conf.example /etc/3am/gate.conf # then edit it
visudo -f /etc/sudoers.d/threeam # from sudoers.example
install -d -o threeam -g threeam -m 0700 /home/threeam/.ssh
echo 'restrict,command="/usr/local/bin/3am-gate" ssh-ed25519 AAAA... 3am' > /home/threeam/.ssh/authorized_keys
chown threeam:threeam /home/threeam/.ssh/authorized_keys && chmod 0600 /home/threeam/.ssh/authorized_keysrestrict turns off port, agent and X11 forwarding and terminals for that key. The gate needs only POSIX sh,
systemctl, journalctl and coreutils.
2. Say what 3AM may read and do
log /var/log/payments/*.log
unit payments.service
path /opt/payments/*.jar
java_user payments
action restart_service unit=^payments\.service$ :: sudo -n /usr/bin/systemctl restart {unit}
action clean_rotated_logs dir=^/var/log/payments$ :: find {dir} -name '*.log.*' -type f -mtime +0 -deletethreeam ALL=(root) NOPASSWD: /usr/bin/systemctl restart payments.service
threeam ALL=(payments) NOPASSWD: /usr/bin/jcmdlog, unit and path say what 3AM may read. java_user is the account Java services run as, for thread dumps. The
packs use the action names restart_service and clean_rotated_logs: keep them, and change the patterns and commands
to fit your servers. An action that isn't listed can't run, whatever 3AM asks for.
3. Add the connector
{ "name": "servers", "kind": "ssh",
"settings": { "hosts": ["app-01.bank.internal"], "user": "threeam",
"key_file": "/etc/3am/secrets/3am-ssh-key", "known_hosts": "/etc/3am/secrets/known_hosts",
"apps": { "payments": { "hosts": ["app-01.bank.internal"], "unit": "payments.service",
"logs": ["/var/log/payments/app.log"], "artifact": "/opt/payments/app.jar",
"port": 8080, "java": true, "start_marker": "payments starting" } } } }known_hosts pins each server's host key. A key that changes is refused, and 3AM opens a HostKeyChanged incident
instead of connecting. start_marker is a line the application logs when it starts. 3AM reads only the current run's
logs, so yesterday's error isn't mistaken for today's cause.
kind: sshactionsalerts in| Setting | What to enter | Required | Default |
|---|---|---|---|
hosts | Host names or IPs (each runs 3am-gate) | yes | — |
user | SSH user | yes | "threeam" |
key_filesecret | 3AM's private key | yes | — |
known_hosts | known_hosts with the hosts' keys (pinned: a changed key is refused) | yes | — |
port | SSH port | yes | 22 |
jump_host | Bastion (user@host), if the hosts are only reachable through one | no | — |
apps | Applications: {name: {hosts, unit, logs, artifact, port, java, start_marker}} (JSON) | no | {} |
watch_failed_units | Raise ServiceUnitFailed for any failed unit (else only the apps' units) | yes | false |
disk_pct | Disk use (%) that makes an incident | yes | 92 |
unreachable_after_s | Seconds unreachable before it's an incident | yes | 120 |
log_lines | Log lines read per look | yes | 300 |
4. Verify
1 host through 3am-gate (v1), 1 appIf a server's gate.conf allows an action that 3AM refuses to run, Test connection names it.
What it detects
| Signal | Meaning |
|---|---|
AppDown | An application's unit isn't active, with its result and exit status |
HostUnreachable | SSH hasn't answered for unreachable_after_s (2 min): the name, sshd, or the whole host |
HostKeyChanged | A server's host key changed. 3AM stops connecting until a person checks |
SshAuthFailing | A server refuses 3AM's key |
HostDiskFull | A file system is above disk_pct (92%) |
ServiceUnitFailed | Any failed unit, with watch_failed_units on |
Probe and monitoring alerts that carry app and host labels (SyntheticProbeFailing, EndpointDown, ...) reach the
same causes. That's how 3AM finds a deadlocked service: it's running, but not answering.
Runbooks
When the checks find the error line, 3AM searches your knowledge, such as ServiceNow knowledge bases and the runbooks in your repositories, for that exact error. The matching articles are named in the approval request and the incident note.
Certification
Live, on a real Linux server (Ubuntu 24.04 with systemd and sshd, a Java service on OpenJDK 21, sudo rules and the gate), with 3AM inside a Kubernetes cluster reaching it only through the gate. Every scenario passed (2026-10-05):
- The guardrails. 3AM reached the server only through the gate. It refused
reboot_now, although that server's gate.conf allows it, and a restart of a unit outside the allowed pattern. Seven direct attempts with 3AM's own key to get past the gate (a shell,/etc/shadow, a path traversal, a disallowed unit,;id) were all refused by the server. Port forwarding was refused, and so was a file the gate doesn't list. A password in the service's log reached 3AM masked. - A crash. Nothing in the logs a restart wouldn't fix: restarted through the gate, and verified running.
- Out of memory. Proven from the
OutOfMemoryErrorline and restarted. The knowledge base article for that error was cited in the approval request and the incident note. - A full disk. Its old rotated logs were deleted (the action that server allows), then it was restarted.
- Its port taken. Advice naming the holder, though 3AM's unprivileged account can't see another user's process.
- A bad deploy. Advice to roll back, and no restart into the broken build.
- Its database down. Read from the service's own log, checked on the database: advice, and the service left alone.
- Its database back, the service given up. Restarted.
- A deadlock. Running but not answering: restarted, with the thread dump kept for the developers.
- The host key changed. A security incident, and 3AM stopped connecting to the server.
Troubleshooting
| You see | Do this |
|---|---|
its host key changed | Confirm the server was rebuilt, from its console, then update 3AM's known_hosts |
it refuses 3AM's key | Check 3AM's line in authorized_keys, and that the account isn't locked or expired |
refused by 3am-gate: unit not allowed | Add the unit to gate.conf (unit ...) |
isn't allowed on <host> (its gate.conf doesn't list it) | Add the action to that server's gate.conf, if 3AM should be allowed to run it |
3AM refuses it (looks destructive) | Rewrite the action without the destructive command. 3AM won't run it on any server |