A production-ready shell script that automates the setup of unattended security updates on Ubuntu and Debian servers. Keeps your systems patched against known vulnerabilities with zero manual intervention.
Run as root:
sudo bash -c "$(curl -fsSL https://github.com/ruhanirabin/unattended-setup-script-debian/raw/main/setup_auto_updates.sh)"For non-interactive (automation/Ansible/Packer):
sudo bash setup_auto_updates.sh --yes- Root access (sudo or root user)
- Internet connectivity for package installation
- Minimum 50 MB free disk space
Container note: The script detects the absence of systemd and skips service enablement, but still writes configuration files. You can use cron or a container-native scheduler instead.
| Distribution | Versions | Codenames |
|---|---|---|
| Ubuntu | 24.04 LTS | noble |
| Ubuntu | 24.10 | oracular |
| Ubuntu | 25.04 | plucky |
| Ubuntu | 25.10 | questing |
| Ubuntu | 26.04 LTS | resolute |
| Ubuntu | 26.10 | — |
| Debian | 13 | trixie |
| Debian | 14 | forky |
| Debian | Sid (unstable) | sid |
Note: Unsupported versions will be detected and rejected with a clear error message listing supported releases.
- Root verification — ensures the script runs with elevated privileges
- Distribution detection — identifies OS type, version, and codename via
/etc/os-release - Existing config check — detects running unattended-upgrades and warns about reconfiguration
- Package installation — installs
unattended-upgradesvia apt - Config backup — backs up existing APT configuration files before modification
- Unattended-upgrades configuration — writes distribution-specific config (
50unattended-upgrades) - Schedule configuration — sets daily update/check cycles (
20auto-upgrades) - Dry-run validation — tests the configuration without applying changes
- Service enable/start — enables and starts the
unattended-upgradessystemd service - Status verification — reports service state and provides verification commands
sudo bash setup_auto_updates.shPrompts for confirmation before making changes.
sudo bash setup_auto_updates.sh --yesSkips all prompts. Ideal for automation, Ansible, Packer, or cloud-init.
sudo bash setup_auto_updates.sh --dry-runValidates the environment and shows what would be done without making any changes.
sudo bash setup_auto_updates.sh --yes --verboseEnables detailed debug output on stdout and in the log file.
sudo bash setup_auto_updates.sh --yes --quietMinimal output: only errors and the final STATUS: CHANGED or STATUS: UNCHANGED marker.
sudo bash setup_auto_updates.sh --log-file /var/log/my-setup.log| Flag | Description |
|---|---|
-y, --yes |
Skip all confirmation prompts |
-n, --dry-run |
Validate only, no changes |
-q, --quiet |
Minimal output (errors + status only) |
-v, --verbose |
Enable debug output |
-l, --log-file <path> |
Set custom log file path |
-h, --help |
Show help and exit |
The script is designed to be idempotent — running it multiple times is safe and will not duplicate work:
- Package installation skips if already present
- Config files are compared before writing; unchanged files are left alone
- Service enablement only reports a change if it was not previously enabled
- Final output includes a status marker for automation tools:
STATUS: CHANGED— something was modifiedSTATUS: UNCHANGED— everything was already up to date
Use --quiet with --yes for Ansible, Packer, cloud-init, or CI/CD pipelines.
A ready-to-use Ansible role, sample inventory, and playbook are included in the ansible/ directory. The layout follows standard Ansible conventions so it can be run manually, via cron, or imported into tools like Ansible Semaphore / AWX.
ansible/
├── ansible.cfg # Ansible settings (become, inventory path, forks)
├── inventory/
│ └── hosts.yml # Sample YAML inventory
├── playbooks/
│ └── site.yml # Main entry-point playbook
└── roles/
└── unattended_updates/
├── defaults/main.yml # Default variables (safe to override)
├── handlers/main.yml # Service restart handler
├── tasks/main.yml # Role tasks
└── templates/
├── 20auto-upgrades.j2
└── 50unattended-upgrades.j2
- Ansible 2.15+ on the control node.
- SSH key-based access (or password with
ansible_ssh_pass) to target hosts. - Sudo / root privileges on targets (
become: trueis set inansible.cfg). - Targets must run Ubuntu 24.04+ or Debian 13+.
The included inventory/hosts.yml is a minimal example. Adapt it to your environment:
all:
children:
homelab:
vars:
ansible_user: root
hosts:
node-02-web:
ansible_host: 192.168.68.10
node-02-db:
ansible_host: 192.168.68.11
vps:
vars:
ansible_user: root
hosts:
vps-hermes:
ansible_host: 203.0.113.5You can also use a static INI file if you prefer:
[homelab]
node-02-web ansible_host=192.168.68.10 ansible_user=root
node-02-db ansible_host=192.168.68.11 ansible_user=root
[vps]
vps-hermes ansible_host=203.0.113.5 ansible_user=rootPlace overrides under inventory/group_vars/ or inventory/host_vars/ so they are automatically loaded:
# inventory/group_vars/homelab.yml
---
unattended_updates_auto_reboot: true
unattended_updates_auto_reboot_time: "00:30"# inventory/host_vars/vps-hermes.yml
---
unattended_updates_auto_reboot: falsecd ansible
ansible-playbook playbooks/site.ymlansible-playbook playbooks/site.yml --check --diffansible-playbook playbooks/site.yml --limit homelab
ansible-playbook playbooks/site.yml --limit node-02-webansible-playbook playbooks/site.yml -v
ansible-playbook playbooks/site.yml -vvv # Debug-levelansible-playbook playbooks/site.yml -i /path/to/your/inventory.ymlAll variables are defined in ansible/roles/unattended_updates/defaults/main.yml. Override them at the playbook, inventory, group_vars, or command-line level.
| Variable | Default | Description |
|---|---|---|
unattended_updates_auto_reboot |
false |
Reboot automatically if a kernel or critical update requires it |
unattended_updates_auto_reboot_time |
"02:00" |
Reboot schedule in 24-hour format |
unattended_updates_auto_reboot_with_users |
false |
Reboot even when users are logged in |
unattended_updates_dev_release |
"false" |
Allow development release upgrades (keep false) |
unattended_updates_periodic_update_package_lists |
"1" |
Days between apt update runs (1 = daily) |
unattended_updates_periodic_unattended_upgrade |
"1" |
Days between unattended upgrade runs |
unattended_updates_periodic_autoclean_interval |
"7" |
Days between apt autoremove / autoclean runs |
unattended_updates_log_file |
"/var/log/ansible-unattended-setup.log" |
Path for the role’s internal log |
Inside the playbook:
---
- name: Configure automatic security updates
hosts: all
become: true
roles:
- role: unattended_updates
vars:
unattended_updates_auto_reboot: true
unattended_updates_auto_reboot_time: "00:30"On the command line:
ansible-playbook playbooks/site.yml \
-e "unattended_updates_auto_reboot=true" \
-e "unattended_updates_auto_reboot_time=04:00"The role is fully idempotent:
- APT cache is only updated if older than 1 hour.
- Configuration files are backed up before changes.
- The systemd service is restarted only when configs change (handler).
- The dry-run step reports
changed_when: falseand is allowed to fail gracefully on systems with no pending updates.
This repository is compatible with Ansible Semaphore and Red Hat Ansible Automation Platform:
- Add this repository as a Project / Task Template source.
- Set the playbook path to
ansible/playbooks/site.yml. - Point the inventory to your own inventory file or use the sample at
ansible/inventory/hosts.yml. - The role automatically detects Ubuntu vs Debian and applies the correct origin patterns.
Run the playbook automatically from your control node at midnight:
0 0 * * * cd /opt/unattended-setup-script-debian/ansible && \
git pull origin main >/dev/null 2>&1 && \
ansible-playbook playbooks/site.yml >> /var/log/ansible-nightly.log 2>&1If you prefer invoking the shell script instead of the role:
- name: Set up unattended security updates
ansible.builtin.script: ../setup_auto_updates.sh
args:
creates: /etc/apt/apt.conf.d/50unattended-upgrades
register: unattended_setup
changed_when: "'STATUS: CHANGED' in unattended_setup.stdout"For non-interactive execution, pass --yes:
- name: Set up unattended security updates (non-interactive)
ansible.builtin.command: "bash {{ playbook_dir }}/../setup_auto_updates.sh --yes --quiet"
args:
creates: /etc/apt/apt.conf.d/50unattended-upgrades
register: unattended_setup
changed_when: "'STATUS: CHANGED' in unattended_setup.stdout"The main unattended-upgrades configuration. This script generates a distro-specific file:
- Ubuntu: Uses
Allowed-Originswith${distro_id}:${distro_codename}-securitypatterns - Debian: Uses
Origins-Patternwithorigin=Debian,codename=<codename>patterns
Included settings:
- Security updates only (updates, proposed, backports commented out)
AutoFixInterruptedDpkg "true"— recovers from interrupted package operationsMinimalSteps "true"— safer upgrades resilient to interruptionsRemove-Unused-Kernel-Packages "true"— cleans old kernelsRemove-Unused-Dependencies "true"— removes orphaned packagesAutomatic-Reboot "false"— disabled by default for safety
Enabling automatic reboot:
Edit /etc/apt/apt.conf.d/50unattended-upgrades and change:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Controls the periodic execution schedule:
APT::Periodic::Update-Package-Lists "1"; # Daily
APT::Periodic::Unattended-Upgrade "1"; # Daily
APT::Periodic::AutocleanInterval "7"; # Weekly
After running the script, verify the setup:
# Check service status
systemctl status unattended-upgrades
# View service journal
journalctl -u unattended-upgrades --no-pager -n 50
# Run a dry-run test
sudo unattended-upgrades --dry-run -v
# Check the unattended-upgrades log
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
# View the script's own log
sudo cat /var/log/unattended-setup.logThe script writes structured logs to:
- Default path:
/var/log/unattended-setup.log - Custom path: Use
--log-file /path/to/log
Log format:
[YYYY-MM-DD HH:MM:SS] [LEVEL] message
Levels: INFO, WARN, ERROR, DEBUG
Unattended-upgrades logs (managed by the package itself):
/var/log/unattended-upgrades/unattended-upgrades.log/var/log/unattended-upgrades/unattended-upgrades-dpkg.log
# Check journal for errors
journalctl -u unattended-upgrades -e --no-pager
# Check APT configuration syntax
sudo apt-config dump | grep -i unattendedThis is normal on freshly installed systems with no pending updates. Verify with:
sudo unattended-upgrades --dry-run -v --apt-debugThe script validates the OS version against a supported list. Update the script or file an issue if your version should be supported.
Ensure no conflicting files exist in /etc/apt/apt.conf.d/:
ls -la /etc/apt/apt.conf.d/ | grep -E '(10periodic|20auto|50unattended)'The script backs up existing files with a .bak.<timestamp> suffix.
The script uses a lock file (/var/run/setup_auto_updates.sh.lock) to prevent concurrent runs. If a previous run crashed without cleaning up:
sudo rm -f /var/run/setup_auto_updates.sh.lockThe script detects the absence of systemd and skips service management. Configuration files are still written. To run unattended-upgrades in a container, use cron instead:
apt-get install -y cron
(crontab -l 2>/dev/null; echo "0 6 * * * /usr/bin/unattended-upgrade --dry-run") | crontab -See the Uninstallation section below.
- Automatic updates can introduce regressions. Test on non-production systems first
- The script installs security updates only by default — it does not apply feature updates or backports
- Kernel updates that require a reboot will not automatically reboot the system (configurable)
- Always review scripts from the internet before piping them to
bash - Configuration files are backed up before modification
- The
AutoFixInterruptedDpkgoption helps recover from interrupted upgrades but may mask underlying package issues
To reverse all changes made by this script:
# 1. Stop and disable the service
sudo systemctl stop unattended-upgrades
sudo systemctl disable unattended-upgrades
# 2. Remove configuration files
sudo rm -f /etc/apt/apt.conf.d/20auto-upgrades
sudo rm -f /etc/apt/apt.conf.d/50unattended-upgrades
# 3. Restore backups (if available)
sudo mv /etc/apt/apt.conf.d/*.bak.* /etc/apt/apt.conf.d/ 2>/dev/null || true
# 4. Remove the package (optional)
sudo apt-get remove --purge -y unattended-upgrades
sudo apt-get autoremove -y
# 5. Remove the setup log
sudo rm -f /var/log/unattended-setup.logSee CHANGELOG.md for the full release history.
This project is licensed under the MIT License. See LICENSE for details.
Ruhani Rabin
Issues and pull requests are welcome. Please test any changes on both Ubuntu and Debian before submitting.