An open API service indexing awesome lists of open source software.

https://github.com/ruhanirabin/unattended-setup-script-debian

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.
https://github.com/ruhanirabin/unattended-setup-script-debian

debian shell-script ubuntu unattended-upgrades

Last synced: about 1 month ago
JSON representation

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.

Awesome Lists containing this project

README

          

# Automatic Security Updates Setup Script

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-3.2.0-blue.svg)](CHANGELOG.md)
[![Ubuntu](https://img.shields.io/badge/Ubuntu-24.04%20%7C%2024.10%20%7C%2025.04%20%7C%2025.10%20%7C%2026.04%20%7C%2026.10-E95420)](#)
[![Debian](https://img.shields.io/badge/Debian-13%20%7C%2014%20%7C%20Sid-A81D33)](#)

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.

---

## Quick Install

Run as **root**:

```bash
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):

```bash
sudo bash setup_auto_updates.sh --yes
```

---

## Requirements

- **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.

---

## Supported Distributions

| 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.

---

## What This Script Does

1. **Root verification** — ensures the script runs with elevated privileges
2. **Distribution detection** — identifies OS type, version, and codename via `/etc/os-release`
3. **Existing config check** — detects running unattended-upgrades and warns about reconfiguration
4. **Package installation** — installs `unattended-upgrades` via apt
5. **Config backup** — backs up existing APT configuration files before modification
6. **Unattended-upgrades configuration** — writes distribution-specific config (`50unattended-upgrades`)
7. **Schedule configuration** — sets daily update/check cycles (`20auto-upgrades`)
8. **Dry-run validation** — tests the configuration without applying changes
9. **Service enable/start** — enables and starts the `unattended-upgrades` systemd service
10. **Status verification** — reports service state and provides verification commands

---

## Usage

### Interactive Mode (default)

```bash
sudo bash setup_auto_updates.sh
```

Prompts for confirmation before making changes.

### Non-Interactive Mode

```bash
sudo bash setup_auto_updates.sh --yes
```

Skips all prompts. Ideal for automation, Ansible, Packer, or cloud-init.

### Dry-Run Mode

```bash
sudo bash setup_auto_updates.sh --dry-run
```

Validates the environment and shows what would be done without making any changes.

### Verbose Mode

```bash
sudo bash setup_auto_updates.sh --yes --verbose
```

Enables detailed debug output on stdout and in the log file.

### Quiet Mode (CI/CD / Ansible)

```bash
sudo bash setup_auto_updates.sh --yes --quiet
```

Minimal output: only errors and the final `STATUS: CHANGED` or `STATUS: UNCHANGED` marker.

### Custom Log File

```bash
sudo bash setup_auto_updates.sh --log-file /var/log/my-setup.log
```

### All Options

| 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 ` | Set custom log file path |
| `-h`, `--help` | Show help and exit |

---

## Automation & Idempotency

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 modified
- `STATUS: UNCHANGED` — everything was already up to date

Use `--quiet` with `--yes` for Ansible, Packer, cloud-init, or CI/CD pipelines.

---

## Ansible Integration

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.

### Directory Layout

```
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
```

### Prerequisites

- **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: true` is set in `ansible.cfg`).
- Targets must run **Ubuntu 24.04+** or **Debian 13+**.

### Inventory Setup

The included `inventory/hosts.yml` is a minimal example. Adapt it to your environment:

```yaml
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.5
```

You can also use a static INI file if you prefer:

```ini
[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=root
```

#### Group / Host Variables

Place overrides under `inventory/group_vars/` or `inventory/host_vars/` so they are automatically loaded:

```yaml
# inventory/group_vars/homelab.yml
---
unattended_updates_auto_reboot: true
unattended_updates_auto_reboot_time: "00:30"
```

```yaml
# inventory/host_vars/vps-hermes.yml
---
unattended_updates_auto_reboot: false
```

### Running the Playbook

#### Basic Deploy (All Hosts)

```bash
cd ansible
ansible-playbook playbooks/site.yml
```

#### Check Mode (Dry Run)

```bash
ansible-playbook playbooks/site.yml --check --diff
```

#### Limit to a Group or Single Host

```bash
ansible-playbook playbooks/site.yml --limit homelab
ansible-playbook playbooks/site.yml --limit node-02-web
```

#### Verbose Output

```bash
ansible-playbook playbooks/site.yml -v
ansible-playbook playbooks/site.yml -vvv # Debug-level
```

#### Custom Inventory File

```bash
ansible-playbook playbooks/site.yml -i /path/to/your/inventory.yml
```

### Role Variables

All 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 |

#### Override Examples

**Inside the playbook:**

```yaml
---
- 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:**

```bash
ansible-playbook playbooks/site.yml \
-e "unattended_updates_auto_reboot=true" \
-e "unattended_updates_auto_reboot_time=04:00"
```

### Idempotency

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: false` and is allowed to fail gracefully on systems with no pending updates.

### Ansible Semaphore / AWX

This repository is compatible with [Ansible Semaphore](https://www.semui.co/) and Red Hat Ansible Automation Platform:

1. Add this repository as a **Project** / **Task Template** source.
2. Set the playbook path to `ansible/playbooks/site.yml`.
3. Point the inventory to your own inventory file or use the sample at `ansible/inventory/hosts.yml`.
4. The role automatically detects Ubuntu vs Debian and applies the correct origin patterns.

### Cron Automation (Control Node)

Run the playbook automatically from your control node at midnight:

```cron
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>&1
```

### Using the Shell Script Directly in Ansible

If you prefer invoking the shell script instead of the role:

```yaml
- 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`:

```yaml
- 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"
```

---

## Configuration Details

### `/etc/apt/apt.conf.d/50unattended-upgrades`

The main unattended-upgrades configuration. This script generates a distro-specific file:

- **Ubuntu**: Uses `Allowed-Origins` with `${distro_id}:${distro_codename}-security` patterns
- **Debian**: Uses `Origins-Pattern` with `origin=Debian,codename=` patterns

**Included settings:**
- Security updates only (updates, proposed, backports commented out)
- `AutoFixInterruptedDpkg "true"` — recovers from interrupted package operations
- `MinimalSteps "true"` — safer upgrades resilient to interruptions
- `Remove-Unused-Kernel-Packages "true"` — cleans old kernels
- `Remove-Unused-Dependencies "true"` — removes orphaned packages
- `Automatic-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";
```

### `/etc/apt/apt.conf.d/20auto-upgrades`

Controls the periodic execution schedule:

```
APT::Periodic::Update-Package-Lists "1"; # Daily
APT::Periodic::Unattended-Upgrade "1"; # Daily
APT::Periodic::AutocleanInterval "7"; # Weekly
```

---

## Post-Installation Verification

After running the script, verify the setup:

```bash
# 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.log
```

---

## Logging

The 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`

---

## Troubleshooting

### Service not starting

```bash
# Check journal for errors
journalctl -u unattended-upgrades -e --no-pager

# Check APT configuration syntax
sudo apt-config dump | grep -i unattended
```

### Dry-run test fails

This is normal on freshly installed systems with no pending updates. Verify with:

```bash
sudo unattended-upgrades --dry-run -v --apt-debug
```

### "Unsupported distribution" error

The script validates the OS version against a supported list. Update the script or file an issue if your version should be supported.

### Configuration not applying

Ensure no conflicting files exist in `/etc/apt/apt.conf.d/`:

```bash
ls -la /etc/apt/apt.conf.d/ | grep -E '(10periodic|20auto|50unattended)'
```

The script backs up existing files with a `.bak.` suffix.

### "Another instance is already running" error

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:

```bash
sudo rm -f /var/run/setup_auto_updates.sh.lock
```

### Running in Docker / containers

The 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:

```bash
apt-get install -y cron
(crontab -l 2>/dev/null; echo "0 6 * * * /usr/bin/unattended-upgrade --dry-run") | crontab -
```

### Reverting changes

See the [Uninstallation](#uninstallation) section below.

---

## Security Considerations

- **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 `AutoFixInterruptedDpkg` option helps recover from interrupted upgrades but may mask underlying package issues

---

## Uninstallation

To reverse all changes made by this script:

```bash
# 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.log
```

---

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for the full release history.

---

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.

---

## Author

**Ruhani Rabin**

---

## Contributing

Issues and pull requests are welcome. Please test any changes on both Ubuntu and Debian before submitting.