Commit Graph
28 Commits
Author SHA1 Message Date
jcoffey-dev 96774aba35 Finish the pass: log_inspect, service_manager, rsync_magic, disk_cleanup
log_inspect.sh discarded grep's stderr and ignored its exit status, so
an unprivileged search over root-owned logs was indistinguishable from a
search that genuinely found nothing. grep's three outcomes now mean
three different things: matched, matched nothing, or could not read
everything -- the last of which says so and exits non-zero. Confirmed
grep returns 2 rather than 1 in that case, which is why the naive
"status -eq 1" check would never have fired.

service_manager.sh validates the action before dispatch and requires
root for the five that change system state, leaving status and list open
to anyone. $action is quoted at both call sites.

rsync_magic.sh had --inplace on unconditionally. It writes straight into
destination files instead of to a temporary and renaming, so an
interrupted run leaves them partially overwritten -- the opposite of
what a backup tool should guarantee. Now opt-in, with a warning when
used. Its log lives under /var/log and every line pipes through tee, so
under pipefail an unprivileged run died on the first line with a bare
permission error; it now falls back to stdout rather than failing the
sync over its own logging. --delete also confirms before running, since
reversing the two arguments erases the backup.

disk_cleanup.sh moves from `set -o pipefail` to full strict mode, with
the two pipelines that legitimately return non-zero handled at their
call sites rather than by leaving the script lax. Its "largest files"
walk also gained -xdev, which it was missing while security_audit.sh
next door already had it -- without it the walk descends /proc, /sys and
every network mount.

All fifteen scripts now run under set -euo pipefail.
2026-08-22 22:24:50 -07:00
jcoffey-dev f8a86736f9 Harden user_manage, update_system, and the Zimbra pair
user_manage.sh ran useradd/userdel/usermod with no privilege check at
all, so an ordinary user got "Failed to create user." with no hint that
root was the missing piece. Mutating subcommands now require root while
listusers/listgroups stay open, and the check runs *after* the
subcommand is recognised so a bare invocation still prints usage instead
of complaining about privileges. Account names are validated before
reaching useradd, and `deluser` -- which removes the home directory
irrecoverably -- prints what it will delete and confirms first.

zimbra_backup.sh reported success on failed backups. getRestURL can
write an HTTP error body and still exit zero, so a " Backup completed"
could sit over a file containing an error page. Size and gzip -t checks
now gate that, and a suspect file is renamed .suspect rather than
deleted, so it can be looked at. zimbra_restore.sh likewise validates
the archive before starting a restore from it.

Both Zimbra scripts had `cmd` followed by `if [ $? -eq 0 ]`. Adding
set -e to those would have made the error branches unreachable -- set -e
exits before the check -- so the tests are inline instead. That would
have been a silent regression rather than a visible one.

update_system.sh gained strict mode, and a note on the pacman branch:
Arch has no supported partial-upgrade path, and --noconfirm answers away
the prompts that would otherwise warn.

Integrity checks verified against an error page, a truncated archive, a
non-gzip file and a real one.
2026-08-22 22:22:05 -07:00
jcoffey-dev 1e676bb634 Make backup and restore safe to actually rely on
backup.sh named archives with %Y%m%d, so a second run on the same day
silently overwrote the first -- destroying a good backup at the moment
someone was trying to take another. Names now carry seconds, and an
existing path is refused rather than clobbered. The archive is also
written to a .partial name and renamed only on success, with a trap to
clean up, so an interrupted run cannot leave a truncated file that looks
like a backup.

restore.sh extracted with tar's defaults. An archive is untrusted input
-- whoever produced it chooses the paths, ownership and modes inside it
-- and extracting as root let the tarball dictate uid/gid and restore
setuid bits directly. Now --no-same-owner --no-same-permissions, with -P
still absent so tar keeps stripping leading "/" and refusing ".."
members. It also lists what it is about to extract and confirms first,
since it silently overwrote whatever was already in the target.

Both scripts also gained set -euo pipefail, and both replaced the
`cmd; if [ $? -eq 0 ]` pattern with a direct `if cmd; then`, which is
what that idiom was reaching for and gets wrong as soon as any command
is inserted between the two lines.

Verified end to end: two same-second-apart backups both survive, no
.partial residue, restore refuses non-interactively, and the round trip
diffs identical.
2026-08-22 22:20:04 -07:00
jcoffey-dev e8c0a73dba Harden the four monitoring scripts
set -euo pipefail across all four, but added deliberately rather than
pasted in -- each script needed the places where a non-zero exit is
normal handled first, or strict mode would have made them worse:

- sys_monitor / process_monitor: `ps | head -n 6` is a latent SIGPIPE.
  head closes the pipe after six lines, and on a host with enough
  processes ps fills the buffer and exits 141, which pipefail turns into
  a script abort -- on exactly the busy machine you wanted to inspect.
  Confirmed the mechanism (a large producer into head returns 141) and
  those pipelines now tolerate it.
- security_audit: find exits non-zero when it cannot read a directory,
  which is routine when walking the whole filesystem. Without handling,
  set -e aborted the audit part way while still looking complete. Also
  notes that a clean report as non-root means little, since find cannot
  descend where it may not read.
- network_info: iptables needs root, so the last section aborted the
  script for ordinary users. Now reports the failure, and falls back to
  nft where iptables is absent.

process_monitor also no longer kills on sight. `pkill -x` by name can
match several processes at once, and as root that is an easy way to take
down more than intended. It now prints what it matched and asks, with
FORCE=1 for unattended use and a refusal rather than a hang when there
is no tty.

All four run clean; the kill path was tested against a live process and
left it alive.
2026-08-22 22:19:05 -07:00
jcoffey-dev 82528a1402 Fix the two ShellCheck warnings and hold CI at that level
The workflow went in at severity: error on the assumption that a
never-linted repository would have a backlog worth grandfathering. It
did not -- error found nothing, and warning found exactly two things, so
the cautious setting was protecting against a problem that was not
there.

zimbra_backup.sh: SC2024, sudo does not affect redirects. The
`> "$BACKUP_FILE"` runs as root rather than as the sudo'd zimbra user,
so backups landed root-owned inside a directory the script deliberately
chowns to zimbra:zimbra. Kept the redirect -- root can always write
there, and piping into `tee` would put tee's status in $? and hide a
zmmailbox failure -- and handed ownership over explicitly afterwards.
The suppression is narrow and states why.

disk_cleanup.sh: SC2034, total_freed was assigned and never read.

CI now holds at warning with a clean tree, so anything that trips it is
new rather than inherited.
2026-08-22 22:15:21 -07:00
jcoffey-dev dc55baa63e ci: probe shellcheck at warning severity 2026-08-22 22:13:54 -07:00
jcoffey-dev 95ec7f4a45 ci: add a ShellCheck workflow
Every file here is a shell script that the README invites people to run
as root, and nothing has been linting them: .github/workflows was
deleted in 5eadf64 and never replaced. The bugs found in this week's
audit -- values interpolated into a command string, an unguarded
find -delete, exit statuses read from $? after the fact -- are largely
the class a linter catches for free.

Starts at severity: error deliberately. The existing scripts have never
been through a lint pass, so failing on warnings from day one would
block every PR on pre-existing findings rather than on new ones. The
intent is to tighten to warning, then to nothing, as the backlog is
cleared.
2026-08-22 22:12:58 -07:00
jcoffey-dev 9be14e9670 Stop log_rotate destroying logs that are still in use
Two problems, both data loss.

It gzipped any *.log older than the threshold. gzip writes the .gz and
unlinks the original, so a daemon holding that file open keeps writing
to an unlinked inode and those writes become unreachable -- the exact
failure real logrotate avoids with copytruncate or a postrotate signal.
We can do neither from here, so files currently held open are now
skipped and left for logrotate, and the run says so. If lsof is missing
the check cannot run, and that is reported rather than assumed safe.

It also deleted every .gz older than a hardcoded 90 days, on every run,
with no flag, no dry-run and no confirmation -- destroying archives on
any host with longer retention. Deletion is now opt-in via --purge-days,
prints what it will remove, and needs an interactive confirmation or
--yes. Without a tty and without --yes it refuses instead of proceeding.

Adds --dry-run, --days, -h, and set -euo pipefail. A bare numeric first
argument still works, so existing `log_rotate.sh 14` callers and cron
entries are unaffected.

Verified against a fixture directory: old logs compressed and recent
ones left alone, archives surviving when --purge-days is absent, purge
refusing non-interactively without --yes, and a file held open by a
running process skipped rather than compressed.
2026-08-22 22:12:31 -07:00
jcoffey-dev db19bef328 Guard disk_cleanup's --dirs, and stop --help hiding it
--dirs accepted any path and fed it to `find -delete` running as root,
with no confirmation: `--clean --dirs /home` removed every file in /home
past the age threshold, and `--dirs /` did it system-wide.

Now refuses protected directories, comparing the readlink -f resolved
path so a symlink or /tmp/../home cannot smuggle one through, and
requires absolute paths. Anything outside the /tmp,/var/tmp defaults
also needs an interactive confirmation -- or --yes, so unattended use
stays possible; without a tty and without --yes it refuses rather than
hanging in cron.

usage() sliced fixed line numbers (head -22 | tail -n +18) and had
already outgrown them, truncating --help mid-list at --age. So --dirs,
the one option that could destroy a system, was the one option --help
never mentioned. Replaced with a sed range that tracks the comment block
wherever it moves.

The first version of the protected-path check let "/" through: it
compared against "${p%/}", which turns the "/" entry into an empty
string that matches nothing. Caught by testing the guard against the
paths it exists to stop, rather than assuming it worked.
2026-08-22 22:11:13 -07:00
jcoffey-dev 1170130dc9 Fix shell injection in the Zimbra backup and restore scripts
Both scripts built a command string by interpolating user input into
bash -c:

    sudo -u zimbra bash -c "... -m '$EMAIL' ..."

The single quotes inside the double-quoted string are not protection --
the outer shell expands $EMAIL first. An address of

    x' ; id ; echo '

closes the quote and runs arbitrary commands. Both scripts require root
and invoke this through sudo -u zimbra, so injected commands execute as
the account that owns the entire mail store. Verified against the exact
quoting pattern before and after the change.

Fixed by single-quoting the script body so nothing is interpolated, and
passing values as positional arguments. The bash -c wrapper is kept
deliberately rather than calling zmmailbox directly, since it may depend
on shell setup and this could not be tested against a live Zimbra.

Two related holes in the same input paths:

- $EMAIL is also part of the backup filename, so a "/" wrote outside
  $BACKUP_DIR. Now validated as a plain address.
- The restore prompt took a filename and concatenated it into a path, so
  "../../etc/shadow" escaped $BACKUP_DIR. Now rejects anything
  containing a separator.

Also switched the backup listing from `ls | grep "$EMAIL"` to a find
with grep -F: unquoted the address was treated as a regex, so "." in it
matched any character.
2026-08-22 22:09:28 -07:00
LINUXexpert.org 4b42bc7a1a Updated disk_cleanup.sh with new content 2026-03-20 09:29:28 -07:00
LINUXexpert.org 9bef611a33 Delete .github/workflows directory 2026-01-22 09:26:31 -07:00
LINUXexpert.org 75fa3d7229 Clean up README by removing badge section
Removed unused badge section from README.
2026-01-22 09:26:00 -07:00
LINUXexpert.org 669a2bc5ee Rename workflow and update job names 2026-01-22 09:25:28 -07:00
LINUXexpert.org 2cc51ab91f Add badges section placeholders to README
Added placeholders for badges section in README.
2026-01-22 09:21:27 -07:00
LINUXexpert.org 85393b824c Create workflow to update README with badges
This workflow updates the README with badges using a scheduled cron job.
2026-01-22 09:20:39 -07:00
LINUXexpert.org 2a62fb2fe5 Update README.md 2025-06-12 16:09:23 -07:00
LINUXexpert.org 12c2785a35 Update zimbra_restore.sh 2025-06-12 16:04:07 -07:00
LINUXexpert.org 8e8ca4a4a3 Update zimbra_backup.sh 2025-06-12 16:03:53 -07:00
LINUXexpert.org 6405d3c324 Update rsync_magic.sh 2025-06-12 16:03:33 -07:00
LINUXexpert.org 6e2c1fc833 Create rsync_magic.sh 2025-06-12 16:00:51 -07:00
LINUXexpert.org 3305b8e8a3 Update README.md 2025-06-11 15:10:54 -07:00
LINUXexpert.org 34b7039619 Create zimbra_restore.sh 2025-06-11 15:09:44 -07:00
LINUXexpert.org 11d2b1cf24 Update README.md 2025-06-11 15:08:06 -07:00
LINUXexpert.org 4cd99be161 Create zimbra_backup.sh 2025-06-11 15:06:24 -07:00
LINUXexpert.org 1a46528e6d Update README.md 2025-05-17 10:07:27 -07:00
LINUXexpert.org b6cb2d31f7 Add files via upload 2025-05-17 10:06:49 -07:00
LINUXexpert.org e7f89c23ca Initial commit 2025-05-17 10:06:21 -07:00