Automated system update notifications for Debian and RHEL-based systems with
Discord, Microsoft Teams, Slack, Matrix, ntfy and Gotify support. Integrates
with unattended-upgrades (Debian/Ubuntu) or dnf-automatic
(RHEL/Fedora/Amazon Linux) to send notifications when security updates are
installed.
Upgrading from 1.x? 2.0 changes how notifications are triggered and where secrets are stored, and fixes a credential-exposure issue. Read CHANGELOG.md and SECURITY.md, then re-run the installer.
- π Multi-Platform: Discord, Microsoft Teams, Slack, Matrix, ntfy, Gotify and generic JSON webhooks β any combination
- π Secrets Done Right: Doppler or a local file, always mode 600 and root-owned, loaded via systemd
EnvironmentFile= - βοΈ Accurate Triggering: Fires when the upgrade run actually finishes, so the report describes that run
- π Auto-Reboot: Optional, with the reboot window derived from your update time
- π§ Intelligent: Distinguishes "5 packages updated" from "12 still pending (3 security)" from "up to date"
- π Monitoring-Ready: Nagios/Icinga check, Prometheus exporter, and a machine-readable state file
- π Timezone-Aware: Detects and configures the system timezone during setup
- π€ Automatable: Every setting can come from the environment β no prompts required
- π₯οΈ Multi-OS: Debian/Ubuntu/Raspbian and RHEL/Rocky/AlmaLinux/Amazon Linux/Fedora
- β Tested: 107 automated tests running against six distributions in CI
Choose your preferred method for storing notification secrets:
Secrets stored in /etc/update-notifier/secrets.conf - no external dependencies.
# 1. Clone and enter directory
git clone https://github.com/ChiefGyk3D/Patch-Gremlin.git
cd Patch-Gremlin
# 2. Run setup (it will prompt for secret storage choice)
sudo ./setup-unattended-upgrades.sh
# 3. Follow prompts:
# - Choose update type (security only / all updates)
# - Choose schedule (daily / weekly)
# - Choose timezone
# - Choose verbose logging (default: quiet)
# - Choose auto-reboot after updates (default: enabled)
# - Choose LOCAL file storage
# - Enter webhook URLs for your platforms
# Done! Test the notification:
sudo /usr/local/bin/update-notifier.shBest for managing multiple servers or sharing secrets across teams.
# 1. Install Doppler CLI
curl -sLf https://cli.doppler.com/install.sh | sh
# 2. Authenticate (both user and root)
doppler login
sudo doppler login
# 3. Setup Doppler project
sudo doppler setup --project your-project --config your-config
# 4. Add secrets (choose platforms you want)
doppler secrets set UPDATE_NOTIFIER_DISCORD_WEBHOOK="https://discord.com/api/webhooks/..."
doppler secrets set UPDATE_NOTIFIER_MATRIX_HOMESERVER="https://matrix.org"
doppler secrets set UPDATE_NOTIFIER_MATRIX_USERNAME="username"
doppler secrets set UPDATE_NOTIFIER_MATRIX_PASSWORD="your-password"
doppler secrets set UPDATE_NOTIFIER_MATRIX_ROOM_ID="!room:matrix.org"
# 5. Clone and run setup
git clone https://github.com/ChiefGyk3D/Patch-Gremlin.git
cd Patch-Gremlin
sudo ./setup-unattended-upgrades.sh
# 6. Follow prompts and choose DOPPLER mode
# You'll need to provide your Doppler service token when prompted
# Done! Test the notification:
sudo /usr/local/bin/update-notifier.shWhen using Doppler mode, the setup will prompt for a service token:
# Create a service token (no expiration)
doppler configs tokens create patch-gremlin-token --max-age 0
# Copy the token (starts with dp.st.) and paste when promptedThe service token allows the notification script to access your secrets without requiring doppler login on the system.
The setup script (setup-unattended-upgrades.sh) will interactively ask you about:
-
Update Type
- Security updates only (recommended)
- All available updates
-
Update Schedule
- Daily (recommended for security)
- Weekly (choose day of week)
- Custom time (default: 02:00)
-
Timezone
- Keep current or select from common timezones
-
Verbose Logging
- Quiet (recommended) - only important messages
- Verbose - detailed DEBUG output
-
Automatic Reboot
- Enabled (recommended) - automatically reboots after kernel or critical library updates
- Disabled - requires manual reboot after kernel updates
- Reboots even with logged-in users when enabled
-
Secret Storage
- LOCAL file - simpler, secrets in
/etc/update-notifier/secrets.conf - DOPPLER - centralized, requires Doppler CLI and service token
- LOCAL file - simpler, secrets in
-
Notification Platforms
- Discord webhook URL
- Microsoft Teams webhook URL
- Slack webhook URL
- Matrix (webhook OR homeserver + username/password)
- At least one required
Every setting can come from the environment. Pass --non-interactive and the
installer never prompts β it fails fast with a clear message if something
required is missing, which makes it safe to drive from Ansible or cloud-init.
sudo -E env \
UPDATE_TYPE=security \
UPDATE_SCHEDULE=daily \
UPDATE_TIME=03:30 \
SYSTEM_TIMEZONE=America/New_York \
AUTO_REBOOT=true \
REBOOT_WITH_USERS=false \
SECRET_MODE=local \
LOCAL_DISCORD_WEBHOOK='https://discord.com/api/webhooks/...' \
./setup-unattended-upgrades.sh --non-interactive| Variable | Values | Default |
|---|---|---|
UPDATE_TYPE |
security, all |
prompt |
UPDATE_SCHEDULE |
daily, weekly |
prompt |
UPDATE_DAY |
Sunβ¦Sat |
Sat |
UPDATE_TIME |
HH:MM, 24-hour |
02:00 |
SYSTEM_TIMEZONE |
e.g. Europe/Berlin |
unchanged |
VERBOSE_LOGGING |
true, false |
false |
AUTO_REBOOT |
true, false |
prompt |
REBOOT_WITH_USERS |
true, false |
false |
ENABLE_HEARTBEAT |
true, false |
false |
SECRET_MODE |
doppler, local |
prompt |
DOPPLER_TOKEN |
dp.st.β¦ |
required for Doppler |
Local-mode endpoints come from LOCAL_DISCORD_WEBHOOK, LOCAL_SLACK_WEBHOOK,
LOCAL_TEAMS_WEBHOOK, LOCAL_MATRIX_*, LOCAL_NTFY_*, LOCAL_GOTIFY_* and
LOCAL_WEBHOOK_URL. At least one is required. Run
./setup-unattended-upgrades.sh --help for the full list.
All values are validated before anything is written, so a malformed
UPDATE_TIME is rejected rather than ending up inside a systemd unit.
Patch Gremlin hooks onto the upgrade unit itself
(ExecStartPost on apt-daily-upgrade.service or dnf-automatic.service),
so the notification always describes the run that just completed.
A scheduled "heartbeat" report is also installed but disabled by default.
Enable it with ENABLE_HEARTBEAT=true to get a report even on days when no
upgrade ran; it is scheduled two hours after the upgrade window so it can
never race it.
# Enable the heartbeat later
sudo systemctl enable --now update-notifier.timerIf you need different secret names (to avoid conflicts with other programs), create config.sh:
cp config.example.sh config.sh
nano config.shEdit to match your Doppler secret names:
# Customize these to match YOUR Doppler secret names
export DOPPLER_DISCORD_SECRET="MY_DISCORD_WEBHOOK"
export DOPPLER_TEAMS_SECRET="MY_TEAMS_WEBHOOK"
export DOPPLER_SLACK_SECRET="MY_SLACK_WEBHOOK"
export DOPPLER_MATRIX_HOMESERVER_SECRET="MY_MATRIX_SERVER"
export DOPPLER_MATRIX_USERNAME_SECRET="MY_MATRIX_USER"
export DOPPLER_MATRIX_PASSWORD_SECRET="MY_MATRIX_PASS"
export DOPPLER_MATRIX_ROOM_ID_SECRET="MY_MATRIX_ROOM"Then run setup with your config:
source config.sh
sudo -E ./setup-unattended-upgrades.shIf using local file storage, secrets are stored in /etc/update-notifier/secrets.conf:
SECRET_MODE="local"
# Discord
DISCORD_WEBHOOK="https://discord.com/api/webhooks/..."
# Microsoft Teams
TEAMS_WEBHOOK="https://outlook.office.com/webhook/..."
# Slack
SLACK_WEBHOOK="https://hooks.slack.com/services/..."
# Matrix - Webhook (if available)
MATRIX_WEBHOOK="https://matrix.example.org/_matrix/webhook/..."
# Matrix - API (recommended)
MATRIX_HOMESERVER="https://matrix.org"
MATRIX_USERNAME="username"
MATRIX_PASSWORD="your-password"
MATRIX_ROOM_ID="!room:matrix.org"Security: This file is automatically created with chmod 600 (owner read/write only).
It is parsed as plain KEY="VALUE" lines, never executed as a shell script: $,
backticks and quotes in a value are kept literally.
- Go to Server Settings β Integrations β Webhooks
- Click "New Webhook" or edit existing
- Copy webhook URL
- Format:
https://discord.com/api/webhooks/{id}/{token}
- Go to channel β More options (β―) β Connectors
- Search for "Incoming Webhook" β Configure
- Give it a name, optionally upload image
- Copy webhook URL
- Format:
https://outlook.office.com/webhook/...
- Go to https://api.slack.com/apps
- Create New App β From scratch
- Add "Incoming Webhooks" feature
- Activate and add to workspace
- Copy webhook URL
- Format:
https://hooks.slack.com/services/...
Getting Your Room ID:
Element Web/Desktop:
- Open the room β Click room name β Settings β Advanced
- Copy "Internal room ID" (starts with
!)
Element Mobile:
- Open room β Tap room name β About
- Room ID is at the bottom
Username Format:
- β
Correct:
username(just the localpart) - β Wrong:
@username:matrix.org(full format)
The script will automatically extract the localpart if needed.
# Test notification (dry run - nothing is sent)
sudo /usr/local/bin/update-notifier.sh --dry-run
# Send a real test notification
sudo /usr/local/bin/update-notifier.sh
# Comprehensive diagnostics
sudo ./diagnose-config.sh
# Health check (exit 0/1/2, for monitoring)
sudo /usr/local/bin/patch-gremlin-health-check.sh
# Full deployment test (exits non-zero if anything failed)
sudo ./test-deployment.sh --skip-live
# Verbose logging on/off/show
sudo ./configure-verbosity.sh --verbose
sudo ./configure-verbosity.sh --quiet
sudo ./configure-verbosity.sh --show
# Machine-readable last-run state
cat /var/lib/patch-gremlin/state
# View logs
sudo journalctl -t patch-gremlin --since "1 day ago"
sudo journalctl -f -t patch-gremlinThe project ships a sandboxed bats suite that never writes outside a temp directory or touches the network:
sudo apt-get install -y bats shellcheck
./tests/run.shsudo /usr/local/bin/patch-gremlin-health-check.sh
# 0 = Healthy
# 1 = Warning (non-critical)
# 2 = Critical (service broken)Use --quiet to suppress output and rely on the exit code alone.
sudo cp monitoring/nagios-check.sh /usr/local/bin/
/usr/local/bin/nagios-check.sh
# OK - Patch Gremlin healthy | last_run_age_hours=3 status=updated pending=0 ...Write to the node_exporter textfile collector directory:
sudo cp monitoring/prometheus-exporter.sh /usr/local/bin/
*/5 * * * * root /usr/local/bin/prometheus-exporter.sh \
> /var/lib/node_exporter/patch_gremlin.prom.tmp \
&& mv /var/lib/node_exporter/patch_gremlin.prom.tmp \
/var/lib/node_exporter/patch_gremlin.promExported metrics:
| Metric | Meaning |
|---|---|
patch_gremlin_health |
1 = healthy, 0 = unhealthy |
patch_gremlin_last_run_timestamp_seconds |
Unix time of the last run |
patch_gremlin_last_notification_success |
1 if the last run delivered |
patch_gremlin_packages_upgraded |
Packages upgraded in the last run |
patch_gremlin_pending_updates |
Packages still awaiting upgrade |
patch_gremlin_pending_security_updates |
Of those, security updates |
patch_gremlin_status_info{status="β¦"} |
Last run status as a label |
sudo ./diagnose-config.shShows:
- Secret storage mode (LOCAL vs DOPPLER)
- Configured webhooks
- Service status
- Timer schedule
- Live notification test
- Troubleshooting tips
/usr/local/bin/
βββ update-notifier.sh # Main notification script
βββ patch-gremlin-health-check.sh # Health check for monitoring
/etc/update-notifier/
βββ env # Mode 600 - secrets / secret names
βββ secrets.conf # Mode 600 - local mode endpoints
/var/lib/patch-gremlin/
βββ state # Last-run status, counts, timestamp
/var/backups/patch-gremlin/ # Config backups (NOT in apt.conf.d)
/etc/systemd/system/
βββ update-notifier.service # Notification service (hardened)
βββ update-notifier.timer # Optional heartbeat (off by default)
βββ apt-daily-upgrade.service.d/ # (Debian) ExecStartPost trigger
β βββ patch-gremlin.conf
βββ apt-daily-upgrade.timer.d/ # (Debian) Schedule override
βββ patch-gremlin.conf
/etc/apt/apt.conf.d/ # (Debian only)
βββ 20auto-upgrades # APT periodic config
βββ 50unattended-upgrades # Unattended-upgrades config
/etc/dnf/automatic.conf # (RHEL only)
/etc/systemd/system/dnf-automatic.service.d/
βββ patch-gremlin.conf # (RHEL only) ExecStartPost trigger
setup-unattended-upgrades.sh # Installer (interactive or --non-interactive)
update-notifier.sh # Notification script (copied to /usr/local/bin)
uninstall.sh # Removal (--all, --purge-backups)
config.example.sh # Template for Doppler secret-name overrides
# User tools
diagnose-config.sh # Detailed diagnostics + troubleshooting
health-check.sh # Monitoring health check (exit 0/1/2)
configure-verbosity.sh # --quiet / --verbose / --show
fix-verbose-now.sh # Deprecated shim for --quiet
test-deployment.sh # On-host verification, exits non-zero on failure
# Monitoring integrations
monitoring/
βββ nagios-check.sh # Nagios/Icinga check
βββ prometheus-exporter.sh # Prometheus textfile-collector metrics
# Tests
tests/
βββ notifier.bats # Log parsing, status, JSON payload validity
βββ setup.bats # Staged installs, validation, secret handling
βββ lifecycle.bats # Install -> configure -> uninstall round trips
βββ monitoring.bats # Health check + Nagios + Prometheus
βββ fixtures/ # Real-format unattended-upgrades / dnf logs
βββ helpers/ # curl, apt, dnf, systemctl stubsCustomize script behavior:
# Testing
PATCH_GREMLIN_DRY_RUN=true # Build but never send (same as --dry-run)
# Behaviour
PATCH_GREMLIN_NOTIFY_ON=changes # Stay quiet when nothing changed
PATCH_GREMLIN_BOT_NAME="Patch Gremlin" # Display name in notifications
PATCH_GREMLIN_HOSTNAME=web01.example.com # Override the reported host name
PATCH_GREMLIN_MAX_PACKAGE_NAMES=20 # Package names listed before "and N more"
# Performance tuning
PATCH_GREMLIN_MAX_LOG_LINES=100 # Log lines inspected (default: 50)
PATCH_GREMLIN_RETRY_COUNT=5 # HTTP retries (default: 3)
PATCH_GREMLIN_RETRY_DELAY=5 # Retry delay seconds (default: 2)
PATCH_GREMLIN_CURL_TIMEOUT=60 # HTTP timeout seconds (default: 30)
# Paths (mainly for testing - see CONTRIBUTING.md)
PATCH_GREMLIN_LOG_FILE=/path/to.log # Override upgrade-log discovery
PATCH_GREMLIN_SECRETS_FILE=/path # Override the secrets file
PATCH_GREMLIN_STATE_DIR=/path # Override the state directoryClient-side retries stop early on a 4xx other than 429, since a rejected payload will not become valid on a retry.
# ntfy (local mode)
sudo -E env SECRET_MODE=local \
LOCAL_NTFY_URL='https://ntfy.sh' LOCAL_NTFY_TOPIC='my-servers' \
./setup-unattended-upgrades.sh --non-interactive
# Gotify
sudo -E env SECRET_MODE=local \
LOCAL_GOTIFY_URL='https://gotify.example.com/message' \
LOCAL_GOTIFY_TOKEN='AxxxxxxxxxxxxxX' \
./setup-unattended-upgrades.sh --non-interactiveSet LOCAL_WEBHOOK_URL to receive a machine-readable payload instead of a
chat-formatted message:
{
"host": "web01",
"status": "updated",
"summary": "3 package(s) updated",
"detail": "β
Updates Applied: 3 package(s)\n libssl3, openssl, curl",
"upgraded_count": 3,
"pending_total": 12,
"pending_security": 2,
"timestamp": "2026-08-21T02:14:07.000Z",
"version": "2.0.1"
}Change log verbosity after installation:
# Interactive toggle
sudo ./configure-verbosity.sh
# Shows current setting, lets you enable/disable verbose DEBUG outputQuiet mode (default):
- Shows package updates installed
- Shows errors if they occur
- Clean, readable logs
Verbose mode:
- Shows detailed DEBUG from unattended-upgrades
- Package checking details
- Origin pattern matching
- Useful for troubleshooting
If Patch Gremlin is already installed, the setup script will detect it and offer options:
Options:
1) Reinstall/Reconfigure (preserves nothing)
2) Update scripts only (keeps configuration)
3) Cancel installation
Choose option 2 to update the scripts while keeping your current configuration.
# Run comprehensive diagnostics
sudo ./diagnose-config.shThis will check:
- Secret storage configuration
- Systemd service setup
- Timer status and schedule
- Network connectivity
- Live notification test
# Check secrets file exists and has content
sudo cat /etc/update-notifier/secrets.conf
# Edit and add webhook URLs
sudo nano /etc/update-notifier/secrets.conf
# Test notification
sudo /usr/local/bin/update-notifier.sh# Re-authenticate
sudo doppler login
sudo doppler setup --project your-project --config your-config
# Verify secrets exist
doppler secrets --only-names
# Check service has token
systemctl show update-notifier.service | grep DOPPLER_TOKEN# Check timer status
sudo systemctl status update-notifier.timer
# Enable and start if needed
sudo systemctl enable --now update-notifier.timer
# View schedule
sudo systemctl list-timers update-notifier*# Check recent logs
sudo journalctl -t patch-gremlin --since "1 hour ago"
# Test with dry run
sudo PATCH_GREMLIN_DRY_RUN=true /usr/local/bin/update-notifier.sh
# Check webhook URLs are correct
# LOCAL mode:
sudo cat /etc/update-notifier/secrets.conf
# DOPPLER mode:
doppler secrets get UPDATE_NOTIFIER_DISCORD_WEBHOOK --plain# Disable verbose logging
sudo ./configure-verbosity.sh
# Choose option 1 (Disable)Check username format - should be just the localpart:
# LOCAL mode:
grep MATRIX_USERNAME /etc/update-notifier/secrets.conf
# Should show: MATRIX_USERNAME="username"
# NOT: MATRIX_USERNAME="@username:matrix.org"
# DOPPLER mode:
doppler secrets get UPDATE_NOTIFIER_MATRIX_USERNAME --plain
# Should show just: username- Run diagnostics:
sudo ./diagnose-config.sh - Check logs:
sudo journalctl -t patch-gremlin --since "1 day ago" - See GitHub Issues
- Join Discord or Matrix
sudo ./uninstall.shThis will remove:
- Notification script from
/usr/local/bin/ - Systemd service and timer
- Configuration files
- Post-upgrade hooks (APT or DNF)
- Local secrets file (if present)
Note: Doppler secrets are NOT removed (they may be used by other systems).
- OS: Debian 12+, Ubuntu 20.04+, RHEL 8+, Rocky 8+, AlmaLinux 8+, Amazon Linux 2023, Fedora 35+
- Access: Root/sudo privileges
- Secrets: At least one notification platform configured
- Optional: Doppler CLI (only if using Doppler mode)
Debian-based:
- Debian 12 (Bookworm), 13 (Trixie)
- Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
- Raspberry Pi OS
- Other Debian derivatives
RHEL-based:
- Red Hat Enterprise Linux 8, 9
- Rocky Linux 8, 9
- AlmaLinux 8, 9
- Amazon Linux 2023
- Fedora 35+
Debian/Ubuntu:
unattended-upgradesruns automatically (via systemd timer or apt-daily)- Security updates are downloaded and installed
- APT post-invoke hook triggers
update-notifier.sh - Script analyzes logs and sends notifications
RHEL/Fedora/Amazon Linux:
dnf-automaticruns automatically (via systemd timer)- Security updates are downloaded and installed
- DNF systemd hook triggers
update-notifier.sh - Script analyzes logs and sends notifications
-
Load Configuration
- Check if LOCAL mode: read
/etc/update-notifier/secrets.conf - Check if DOPPLER mode: use environment variables from systemd service
- Fall back to Doppler CLI if needed
- Check if LOCAL mode: read
-
Analyze Logs
- Parse system logs (syslog or journald)
- Detect actual package updates vs no updates
- Extract package names and counts
- Determine notification color/priority
-
Send Notifications
- Format message for each platform
- Include hostname, timestamp (timezone-aware), log excerpt
- Retry on failure (configurable retries and delays)
- Log success/failure to syslog
-
Clean Exit
- Return appropriate exit code
- Clean up temporary files
- Local secrets: File is
chmod 600(root read/write only) - Doppler secrets: Service token embedded in systemd Environment directives
- No passwords in logs: All credentials are redacted from log output
- HTTPS only: All webhook calls use secure connections
Dual License: MPL-2.0 OR Commercial
- Open Source Use: Licensed under the Mozilla Public License 2.0 (MPL-2.0)
- Commercial Use: IF planning on modifying, requires contacting @ChiefGyk3D
- Most requests approved free of charge with the condition that improvements be contributed back
- See LICENSE for full details
Issues and pull requests welcome! By contributing, you agree to license your contributions under the same dual license (MPL-2.0 / Commercial).
CI. Shell lint and tests, security scanning and releases run through the reusable workflows in git-your-ship-together. Branch protection requires these checks:
ci / CI green(ShellCheck,bash -non four distributions, workflow lint)security / CodeQL,security / Secret scan (gitleaks)andsecurity / Semgrep
The documentation checks are path-filtered to Markdown and are not required.
Ways to contribute:
- π Report bugs
- π‘ Suggest features
- π Improve documentation
- π§ Submit code improvements
- β Star the repository
- π’ Share with others
- GitHub Issues - Bug reports and feature requests
- Discord Server - Real-time chat and support
- Matrix Space - Federated chat alternative
- Watch releases - Get notified of new versions
- Follow development - Track progress on roadmap
- Join discussions - Participate in feature planning
If you find Patch Gremlin useful, consider supporting development:
Made with β€οΈ by ChiefGyk3D
| Mastodon | Bluesky | Twitch | YouTube | Kick | TikTok | Discord | Matrix |
ChiefGyk3D is the author of Patch Gremlin
