Automatic updates
A Conn3x circuit that keeps your hosts patched on their own. It sets up the package manager’s own automatic updates, unattended-upgrades on Debian and Ubuntu and dnf’s automatic updates on Fedora and Rocky Linux, and makes the settings you’d otherwise hand-edit on every box into a handful of inputs: security updates only or everything, whether to reboot, when, and which packages to leave alone.
The updates run from a systemd timer on each host, so they keep going whether or not you run Conn3x again. What Conn3x keeps in step is the configuration: if someone edits it or turns the timer off, the next conn3x plan shows it.
module "auto_updates" { source = "github.com/conn3xorg/circuit-auto-updates?ref=v0.2.0"
hosts = { web = "192.168.1.20", db = "192.168.1.21" } ssh_private_key_file = "~/.ssh/id_ed25519"
security_only = true reboot = "when-needed" reboot_time = "04:30" schedule = "*-*-* 03:00" exclude = ["postgresql*"]}Then run conn3x init and conn3x apply. A second conn3x plan says No changes.
Each host installs updates at about 03:00 and, when one of them needs a reboot, such as a new kernel, reboots at 04:30. PostgreSQL is left for you to upgrade by hand.
For hosts created in the same plan, pass host objects with the host key you generated, so Conn3x checks it without a known_hosts entry. Give each one’s distribution in os too: the circuit picks its packages by distribution, and Conn3x can’t ask a host that doesn’t exist yet. If os turns out not to match the host, Conn3x stops before configuring it. Set os as well when you put depends_on on this module: depends_on holds back reading the hosts until the apply, so without os the plan fails with Invalid for_each argument.
hosts = { for name, d in digitalocean_droplet.app : name => { address = d.ipv4_address host_key = tls_private_key.host[name].public_key_openssh os = "ubuntu" }}To try it on a throwaway container first, see examples/local.
What it does
Section titled “What it does”Before the circuit runs, the conn3x_host data source reads each host’s distribution over SSH. Then, on Debian and Ubuntu:
- Installs
unattended-upgrades. - Writes
/etc/apt/apt.conf.d/20auto-upgrades, which turns on the daily package list refresh and the daily upgrade. - Writes
/etc/apt/apt.conf.d/52conn3x-unattended-upgrades. It replaces the origins the package’s own50unattended-upgradesallows with its own list, and sets the package blacklist and the reboot settings.50unattended-upgradesis left as the package shipped it. - Writes a drop-in for
apt-daily-upgrade.timerwithschedule, and enables and startsapt-daily.timerandapt-daily-upgrade.timer.
On Fedora and Rocky Linux:
- Installs
dnf5-plugin-automaticon Fedora, whose dnf is dnf5, ordnf-automaticon Rocky Linux 10, which still runs dnf4. - Writes
/etc/dnf/automatic.conf. On Fedora, dnf5 reads its defaults from the package’s file in/usr/shareand this file on top. On Rocky Linux, this file replaces the one the package ships, and anything it doesn’t set takes dnf’s built-in default. - Writes a drop-in for
dnf5-automatic.timeron Fedora ordnf-automatic.timeron Rocky Linux withschedule, and enables and starts the timer.
The settings map to each package manager like this:
| Input | unattended-upgrades | dnf automatic |
|---|---|---|
security_only = true |
Origins: the release’s -security suite, and on Ubuntu the ESM security archives |
upgrade_type = security |
security_only = false |
Adds the release itself and its -updates suite |
upgrade_type = default |
reboot |
Automatic-Reboot, which reboots when /var/run/reboot-required exists |
reboot = never or when-needed |
reboot_time |
Automatic-Reboot-Time |
reboot_command = "shutdown -r HH:MM ...", whose outer quotes dnf strips |
exclude |
Package-Blacklist, each glob turned into an anchored regular expression |
excludepkgs in [base] |
A host that needs a reboot waits for reboot_time, 04:00 unless you set it, rather than rebooting as soon as the updates finish.
The schedule drop-in is conn3x-schedule.conf in the timer’s .d directory under /etc/systemd/system. It clears the timer’s OnCalendar and sets yours. Before writing it, the circuit checks schedule with systemd-analyze calendar on the host, so a typo fails the apply rather than leaving the timer without a schedule. Without schedule, the drop-in holds only a comment and the timer keeps the distribution’s: 06:00 for both apt-daily-upgrade and dnf automatic, plus a random delay of up to an hour on each.
All three timers have Persistent=true, so a run missed while the host was off happens soon after it’s back. A new schedule takes effect with systemctl daemon-reload, which doesn’t trigger a missed run. Restarting the timer would, so the circuit doesn’t. Starting a stopped timer does trigger a missed run, so on a host where the timer ran before and was later stopped, the first apply can start an update during the day. With reboot = "when-needed", any reboot it needs still waits for reboot_time.
What it doesn’t cover
Section titled “What it doesn’t cover”- Third-party repositories. On Debian and Ubuntu, only the distribution’s own origins are allowed, so packages from Docker’s, PostgreSQL’s or any other added repository aren’t upgraded. On Fedora and Rocky Linux, dnf upgrades from every enabled repository.
- Notifications. Results go to the journal (
journalctl -u apt-daily-upgradeorjournalctl -u dnf-automatic); no email is sent. - Rebooting after every update.
reboot = "when-needed"reboots only when an update asks for it. - Removing it. Deleting the module leaves the package, the configuration and the timer as they are.
Inputs
Section titled “Inputs”| Name | Type | Default | Description |
|---|---|---|---|
hosts |
any | Required | Hosts to configure, by name. Each value is an address, such as "203.0.113.10", or a host object such as { address = "203.0.113.10", host_key = "ssh-ed25519 AAAA..." }. A host object can also set user, port, private_key_file, jump_hosts and os, the distribution’s ID, such as ubuntu or rocky, which a host created in the same plan needs. The two forms can be mixed. |
ssh_user |
string | "root" |
User to log in as. |
ssh_port |
number | 22 |
SSH port. |
ssh_private_key_file |
string | null |
Path to a private key. When null, Conn3x uses the SSH agent. |
ssh_known_hosts_file |
string | null |
known_hosts file to check host keys against. When null, ~/.ssh/known_hosts. Not used for a host object with host_key. |
ssh_become |
bool | false |
Run commands as root through sudo. Set it when ssh_user isn’t root. |
security_only |
bool | true |
Install security updates only. Set it to false to install every update from the distribution’s own repositories. |
reboot |
string | "never" |
"never", or "when-needed" to reboot when an update asks for it. |
reboot_time |
string | "04:00" |
Time of day to reboot, as HH:MM in the host’s time zone. Used when reboot is "when-needed". |
schedule |
string | null |
When the upgrade timer runs, as a systemd OnCalendar expression such as "*-*-* 03:00" or "Sun 04:00". When null, the distribution’s own schedule. |
exclude |
list(string) | [] |
Packages never to update automatically, by name or glob, such as ["postgresql*", "linux-image-*"]. |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
timers |
The systemd timer that installs updates on each host, such as { web = "apt-daily-upgrade.timer", db = "dnf-automatic.timer" }. |
Requirements
Section titled “Requirements”Requires Conn3x v0.6.5 or later and systemd on each host. Debian and Ubuntu hosts must be Debian or Ubuntu themselves, not a derivative, since the allowed origins name them. Hosts need to reach their package mirrors.
Tested on
Section titled “Tested on”Rocky Linux 10, Fedora 45, Ubuntu 24.04 and Debian 13. CI applies examples/local to each on amd64, checks that the second plan has no changes, that the upgrade timer is enabled, active and set to the example’s schedule, and that the package manager reads the example’s settings: with apt-config dump and an unattended-upgrade --dry-run on Debian and Ubuntu, and by reading /etc/dnf/automatic.conf back through dnf’s own parser on Fedora and Rocky Linux, including the reboot command.
The CI containers run with SELinux off. On a Rocky Linux 10 droplet with SELinux enforcing, the example was applied by hand with reboot = "never" and the same checks passed. One run of dnf-automatic.service then installed the pending updates without rebooting, and the audit log had no denials. On Fedora 44 and AlmaLinux 10 droplets, also enforcing, the apply and second plan were clean and the timer was enabled, before and after a reboot; Fedora 44 names it dnf5-automatic.timer, with dnf-automatic.timer as an alias.
License
Section titled “License”MPL-2.0. See LICENSE.
Source
Section titled “Source”- Repository: conn3xorg/circuit-auto-updates. This page covers v0.2.0; the repository’s tags list every release.
- Examples: examples/local.
- Related circuits: Baseline, Hardening, Firewall, fail2ban, OpenSCAP, auditd, Trivy, CISO Assistant, ZAP.