Useful commands
The checks an operator runs most often, as ready-to-paste one-liners: the version and the self-diagnostics on a broker, the same across the whole fleet, the resolved lockdown level of a target, which broker currently holds the shared address, the collected break-glass log and its rotation, the Ansible audit run, and the break-glass drill. After those, the operations the controller drives: the decision on an approval request, the services on the infrastructure hosts, the scheduled jobs, the rolling upgrade, a configuration rollout, the store backup with its restore drill, and the bundled component versions. Web Access has its own block: the system-check rows, the application sessions and the locks. Where it helps to know what to look for, a section shows an output sample with the same placeholders.
Every block on this page is environment-independent. <broker>, <target>, <vip name>, <admin user>,
<admin key> and <break-glass key> are placeholders for this installation's own names, and
<env> and <config source> name the Ansible environment and the configuration source directory on
the controller. Because the one-liners carry those placeholders, they are not measured against the
source tree the way the code spans in this portal's prose are: they are runs measured in a
reference environment with the names taken out. What each section says a command shows comes from
one of those runs.
The commands that read the collected log, the rotation configuration or an approval record need
sudo on the broker. Up to the break-glass drill, nothing on this page changes state, and the drill
itself is only a real login. The sections after it do change state, on purpose: an upgrade, a
rollout, a backup — each says what it touches.
The version and the self-diagnostics on a broker
Run on the broker itself. isg system check prints one row per check with its status marker first,
so the second line keeps only the rows that need attention, and the third only the rows about
identity, configuration, the CMDB and break-glass.
isg --version | head -1
isg system check 2>&1 | grep -E '^\[(WARN|FAIL)\]'
isg system check 2>&1 | grep -E '^\[[A-Z ]+\] (ldap\.yaml|LDAP bind|Config key|cmdb\.yaml|Broker version skew|Break-glass)'
The same across every broker
One ssh per broker with the admin key, so a whole fleet answers on one screen. This is the form
that showed a version skew between two brokers in the reference environment: the rows stand next to
each other, and the one that differs is the one to read.
for h in <broker> <broker>; do ssh -i <admin key> <admin user>@$h 'isg --version | head -1; isg system check 2>&1 | grep -E "^\[[A-Z ]+\] (cmdb\.yaml|Broker version skew)"'; done
The lockdown level of the targets
isg servers level answers for one host at a time by design — a real CMDB connector has no host
list to offer — so a loop is how a whole inventory gets read. The row names the level and where it
came from, which is the part worth reading: a level served from the cache and a level served from
the CMDB look the same otherwise.
for t in <target> <target>; do isg servers level $t 2>/dev/null; done
Which broker holds the shared address
On a keepalived pair exactly one broker carries the shared address at a time. It matters for reading anything else: that broker is the one operators are actually connected through, and the one a rolling upgrade takes last. The address is resolved from its name on the controller, and every broker answers with its own hostname and a plain yes or no.
vip=$(getent hosts <vip name> | awk '{print $1}')
for h in <broker> <broker>; do ssh -i <admin key> <admin user>@$h "hostname; ip -br a | grep -qw $vip && echo 'VIP (<vip name>): yes' || echo 'VIP (<vip name>): no'"; done
<broker-1>
VIP (<vip name>): yes
<broker-2>
VIP (<vip name>): no
The collected break-glass log
Every break-glass login and every forwarded command record lands in
/var/log/isg-breakglass-remote.log on each broker. In order: the line count; the per-day,
per-sending-host breakdown, which reads the month, the day and the host out of the syslog line; the
break-glass lines themselves, counted with the same pattern the forward filter passes; the last
entries; the last successful break-glass logins; and the forwarded command audit (type=EXECVE).
sudo wc -l /var/log/isg-breakglass-remote.log
sudo awk '{print $1, $2, $4}' /var/log/isg-breakglass-remote.log | sort | uniq -c
sudo grep -c -E 'isg-breakglass|isg-bg:|type=EXECVE' /var/log/isg-breakglass-remote.log
sudo tail -5 /var/log/isg-breakglass-remote.log
sudo grep -E '(Accepted|Failed) [a-z]+ for isg-breakglass' /var/log/isg-breakglass-remote.log | tail -3
sudo grep 'type=EXECVE' /var/log/isg-breakglass-remote.log | tail -5
A login line distinguishes the two break-glass layers itself: the certificate path shows up as
ED25519-CERT with the certificate's own ID field, the last-resort static key without either.
The dry run of the log rotation
The rotation of the collected log is size-based, not daily, so the answer to "will it rotate tonight" is a dry run rather than a date. It parses the configuration on the way, which is the other half of what one wants to know.
sudo logrotate -d /etc/logrotate.d/isg-breakglass-remote
The Ansible audit run and its deprecation warnings
verify.yml changes nothing — it asserts, host by host, and a clean run ends with changed=0
everywhere. The DEPRECATION WARNING lines are counted separately because a deprecation is not a
failure and never reaches the recap.
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> playbooks/verify.yml 2>&1 | tee /tmp/verify.out | tail -15
grep -c 'DEPRECATION WARNING' /tmp/verify.out
The break-glass drill on one target
The drill is a real login through the emergency door, with the gateway bypassed entirely: the key is
offered to the isg-breakglass account on the target, and sudo -n true shows that the account
still reaches root without a password. It also leaves the trace the gateway measures — the drill
trace row on isg system check returns to OK only once both break-glass layers have a recent login,
so a drill on the static key alone leaves the row at WARN.
ssh -i <break-glass key> -o IdentitiesOnly=yes isg-breakglass@<target> 'id -un; sudo -n true && echo SUDO-OK'
The decision on an approval request
When a command waits for four-eyes approval, the session prints the request's ID after request=.
Who decided it, when, and with what reason is stored with the request; reading it needs the service
account, because the record store's credentials are readable only to it. A decision made in the web
UI says so in the reason.
sudo setpriv --reuid=isg --regid=isg-operators --clear-groups /usr/local/bin/isg approvals show <approval-id>
State: approved
Requested at: <time>
Decided by: <approver>
Decided at: <time>
Decision reason: <reason> (via admin UI)
Web Access: the rows, the application sessions and the locks
Run on a broker. The first line keeps the three Web Access rows of the system check — the file loads, the shared
accounts' secrets are readable, the listener listens and its certificate covers every application. The second
lists the application sessions (protocol http); reading the record store needs root, like the approval record
above. The third shows the locks, users and applications alike.
isg system check 2>&1 | grep -E '^\[[A-Z ]+\] (apps\.yaml|Web Access)'
sudo env ISG_HOME=/var/lib/isg /usr/local/bin/isg sessions list | grep '(http)'
isg lock list
[OK ] apps.yaml <n> app(s): <app id> <name>→<host> …
[OK ] Web Access credentials <n> account(s) readable
[OK ] Web Access listener listening on [::]:<port>; certificate covers <n> name(s) …
A running application session's transcript is on the serving broker while the session is open, one JSON line per
request (command is the method and the path, exit_code the status the console answered):
sudo tail -n 5 /var/lib/isg/transcript-<session-id>.jsonl
The services on the store, directory and monitoring hosts
The shared store, the directory and the monitoring stack run as rootless containers under one
unprivileged account per host, each container behind a systemd --user unit. One ssh per host
lists both views: the container with its uptime, and the unit with its state.
for h in <store host> <directory host> <monitoring host>; do ssh -i <admin key> <admin user>@$h 'hostname; podman ps --format "{{.Names}} {{.Status}}"; systemctl --user list-units --type=service --no-legend "isg-*" | awk "{print \$1, \$3, \$4}"'; done
<store host>
isg-postgres Up <uptime>
isg-bao Up <uptime>
isg-rustfs Up <uptime>
isg-postgres.service active running
…
The scheduled jobs on the controller
The nightly store backup, the daily check of the newest backup's age (09:00) and the monthly upstream
check run from systemd --user timers on the controller, not on the fleet. The listing shows the
next and the last run; a manual run is the same unit the timer starts, so it measures exactly what
the schedule will do.
systemctl --user list-timers --no-pager 'isg-*'
systemctl --user start isg-upstream-<env>.service; systemctl --user status isg-upstream-<env>.service --no-pager | tail -n 15
A one-shot unit that succeeded reads inactive (dead) with status=0/SUCCESS.
The backup-age check can also run straight from the repository on the controller. It is red when the
newest archive is older than 26 hours or there is none; without -e isg_backup_age_notify=true it
sends no mail.
ansible-playbook -e isg_env=<env> playbooks/backup-age.yml
The rolling upgrade, one broker at a time
On a running fleet the binary is replaced with upgrade-rolling.yml, one broker per run, with a drain
before the restart and a measurement after it — not with site.yml, which would restart every
broker at once. The broker that does not hold the shared address goes first. The binary is a fixed
copy built from the release tag; the play stops before anything is installed if its version does not
match the repository's.
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> -e isg_binary_src=<binary copy> playbooks/upgrade-rolling.yml --limit <broker without the shared address>,<store host>
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> -e isg_binary_src=<binary copy> playbooks/upgrade-rolling.yml --limit <broker with the shared address>,<store host>
The summary at the end of each run is the part to read:
- 'broker B1 (<broker>): <old version> → <new version> (installed before: <old version>; …
- 'fleet compatibility (B401): 1 → 1; …
- 'drain wait: 1 s, 1 poll(s); sessions at drain on: 0'
- 'post-check: isg system check rc=0 (after 1 attempt(s)); [OK ] Broker version skew …
The audit run (verify.yml, above) after the second broker closes the upgrade.
A configuration change, rolled out
A change to the configuration source in the repository reaches the hosts in two steps: the
configuration source on the controller is regenerated from the repository, then the hosts' role runs.
The second run of the same command is the check that the first one finished: it ends with
changed=0.
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> playbooks/config-src.yml
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> -l <group> playbooks/site.yml
<group> is the inventory group of the hosts the change belongs to. A change that reaches the brokers
goes through the rolling upgrade instead (previous section), for the same reason the binary does.
The store backup and the restore drill
The backup run takes the record store, the secret store and the object store into one archive on the controller; the drill restores the newest archive into throwaway containers next to the live ones and measures the restore times, without touching the live store.
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> playbooks/backup.yml
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_config_src_dir=<config source> playbooks/backup-restore-drill.yml
The drill's summary names one restore time per store (RTO) and the row counts it compared.
The bundled component versions
The installed version of every third-party component is read from the hosts; the newest stable
version upstream is asked from the controller alone — the hosts make no outbound call. A component
that is behind is a finding to plan, not a failure.
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_cv_report_dir=<report directory> playbooks/component-versions.yml
ansible-playbook -i inventory/<env>.ini -e isg_env=<env> -e isg_cv_report_dir=<report directory> playbooks/component-upstream.yml
B429 UPSTREAM — 9 komponens; current 8, behind 0, not_measured 1