Repository navigation
common: take Ubuntu's security pocket from security.ubuntu.com - #40
Conversation
Cherry's image takes every pocket from repo.cherryservers.com, which resolves to a mirror inside each datacenter and is re-signed with Cherry's key. One of those mirrors stopped updating for weeks, and `apt-get update` kept hitting the old index without an error, so that box's unattended upgrades quietly found nothing to install. common now writes ubuntu.sources on the Ubuntu boxes: the release, -updates and -backports stay on the mirror as before, and <release>-security comes from security.ubuntu.com, signed with Ubuntu's own keyring, as on a stock install. When the file changes, the package lists are refreshed straight away. Debian (the indexers) is unchanged. node_apt_security_lists_timestamp_seconds, written to the node-exporter textfile directory by an apt Post-Invoke hook on every box, is when the newest security index was published, so a stale mirror shows up as an age. A timestamp, not an age, so it keeps aging when refreshes stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude reviewI found no blocking problems. I read the template, both new task files, the
I did not run any of it, and I did not touch the boxes. No issues found. Two caveats, neither of which blocks the merge:
claude-sonnet-5-5 at high effort · requested by TakGN · re-run with |
TakGN
left a comment
There was a problem hiding this comment.
Two findings from a static review of the APT source migration. I did not run the playbook against the fleet.
| ansible.builtin.apt: | ||
| update_cache: true | ||
| changed_when: false | ||
| when: common_apt_sources is changed |
There was a problem hiding this comment.
[P2] Retry the APT refresh if the source migration was interrupted. The task writes ubuntu.sources before refreshing APT, and the refresh runs only when the template changes. If the refresh fails after the write, a rerun skips it. The regular cache task can also skip it if the old cache is less than an hour old, so the play can continue without an upstream security index. Also trigger the refresh when the configured upstream index is missing.
There was a problem hiding this comment.
Fixed in 3fc54a6. The refresh now runs when ubuntu.sources changed or when /var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_<release>-security_InRelease is missing (the path is derived from common_apt_security_uri). So a run that wrote the file and then failed to refresh refreshes on the next run, whatever the cache age. It also retries three times (10 s apart), as does Update apt cache. If it still fails, the run stops on that box rather than carrying on without a security index.
| It costs a little outbound traffic, and an upstream outage only delays one | ||
| night's run. If a security update needs a newer version of something from | ||
| `-updates` that the mirror does not have yet, apt holds that package back | ||
| until it does; nothing breaks. The URIs and keyrings are `common_apt_*` in |
There was a problem hiding this comment.
[P3] Correct the documented behavior for dependencies available only in -updates. The fleet’s unattended-upgrades policy excludes noble-updates, and unattended-upgrades pins disallowed origins out of dependency resolution. A security upgrade that needs a version available only in -updates can therefore remain pending after Cherry’s mirror catches up. unattended-upgrades implementation
There was a problem hiding this comment.
Right, thanks. unattended-upgrades never installs from -updates here, so a security update that needs a version only -updates has stays pending until someone upgrades it by hand, whether or not the mirror has caught up. Fixed in 3fc54a6 in the README and the PR description. It also says this was already true before this change, and that it is rare, since Ubuntu builds security updates against the release and security pockets.
- Refresh the lists when the upstream security index is missing too, not only when ubuntu.sources changed, so a run that wrote the file and then failed to refresh fixes itself on the next run. Retry that refresh and "Update apt cache" three times. - Add node_apt_lists_refresh_timestamp_seconds, from a stamp our own apt hook touches after a successful refresh (Debian has no update-success-stamp), so a failing refresh can be told from an archive that published nothing new. Both metrics now go to apt-lists.prom. - README: unattended-upgrades never installs from -updates, so a security update that needs a version only there stays pending; an upstream outage stops a site.yml run on that box; the alert should compare boxes first, with a fixed age only once the Date: cadence is known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both nits from the Claude review are addressed in 3fc54a6.
|
TakGN
left a comment
There was a problem hiding this comment.
Follow-up static review. The README now correctly describes -updates dependencies, and reruns retry when the upstream index is missing. Two findings remain.
| delay: 10 | ||
| until: common_apt_refresh is succeeded | ||
| changed_when: false | ||
| when: common_apt_sources is changed or not common_apt_security_index.stat.exists |
There was a problem hiding this comment.
[P2] Verify that the upstream security index exists after refreshing. The new stat runs only before the refresh. If Cherry responds but security.ubuntu.com has a transient network failure, APT can return success with a warning. On a first rollout, the play can then continue without an upstream security index. Check for the expected index after the refresh and fail if it is still missing. APT return behavior
There was a problem hiding this comment.
Fixed in the latest commit. After the refresh, the role stats the upstream index again (the same path, now one set_fact) and asserts it exists. If it does not, the run stops on that box, with a message saying the sources file is in place and to re-run. The next run refreshes again because the index is missing. Both checks are skipped in a dry run, which refreshes nothing.
| group: root | ||
| content: | | ||
| // Managed by Ansible. | ||
| APT::Update::Post-Invoke-Success { "touch {{ common_apt_refresh_stamp }}; /usr/local/sbin/ipni-apt-lists-metric || true"; }; |
There was a problem hiding this comment.
[P2] The refresh metric can report a fresh timestamp during a partial outage. APT runs Post-Invoke-Success when any download succeeds, even if another source fails. If Cherry keeps responding while the Ubuntu security source is unreachable, the stamp keeps advancing. The README also relies on comparing security index dates across hosts, which misses an outage affecting all of them. Either measure the security source’s refresh separately or document that the stamp records partial success. APT hook behavior
There was a problem hiding this comment.
Agreed. Rather than measure each source on its own, I documented what the stamp is: a refresh ran and at least one source answered. That is in the task comment, the metric HELP text, the README table and the PR description. For an outage on every box, which the cross-box comparison cannot see, the README and the PR now add a third alert: the security index older than a generous fixed age (a week), as a backstop to tighten once the Date: cadence is known. One box failing alone still shows in the cross-box comparison.
…p means apt only warns about a source it could not reach when another one answered, so the forced refresh could "succeed" without the upstream security index. Check the index is there afterwards and stop the run on that box if it is not; the next run refreshes again. Post-Invoke-Success runs when any source answered, so the refresh stamp records that a refresh ran and something answered, not that every source did. Say so, and add a generous fixed-age alert on the security index as the backstop for upstream being unreachable from every box at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| ansible.builtin.set_fact: | ||
| common_apt_security_index_path: >- | ||
| /var/lib/apt/lists/{{ common_apt_security_uri | regex_replace('^[a-z]+://', '') | replace('/', '_') | ||
| }}dists_{{ ansible_distribution_release }}-security_InRelease |
There was a problem hiding this comment.
[P2] Normalize the security URI before checking the index path. APT accepts http://security.ubuntu.com/ubuntu and adds the trailing slash. With that override, this expression looks for ubuntudists_…, while APT writes ubuntu_dists_…. The new assertion then stops site.yml even after a successful refresh. Normalize the trailing slash before deriving the filename so both forms work. APT URI normalization
There was a problem hiding this comment.
Fixed in 645216d. The path now strips any trailing slashes and adds exactly one before replacing / with _. Rendered through the role's own set_fact, http://security.ubuntu.com/ubuntu/, http://security.ubuntu.com/ubuntu and https://security.ubuntu.com/ubuntu// all give security.ubuntu.com_ubuntu_dists_noble-security_InRelease. A first attempt with regex_replace('/*$', '/') doubled the slash, because Python's re.sub also matches the empty string at the end.
apt adds a missing trailing slash to a source URI before naming its lists files, so an override without one (http://security.ubuntu.com/ubuntu) gave a path apt never writes and the new check would stop the run after a good refresh. Strip trailing slashes and add exactly one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
Cherry's Ubuntu image takes every pocket,
-securityincluded, fromrepo.cherryservers.com, re-signed with Cherry's own key. That name resolves to a mirror inside each datacenter. One of those mirrors stopped updating for weeks.apt-get updatekept "hitting" the old index without an error, so unattended-upgrades on that box quietly found nothing to install while the other boxes kept getting fixes. Nothing in the repo or in monitoring could show it.Ubuntu's own default takes the security pocket from
security.ubuntu.com, so fixes never wait on a mirror, and that pocket is signed by Ubuntu's own archive key.What
roles/common/tasks/apt_sources.yml, Ubuntu only: templates/etc/apt/sources.list.d/ubuntu.sources(templates/ubuntu.sources.j2).<release>,-updatesand-backportsstay on Cherry's mirror with Cherry's keyring, exactly as now.<release>-securitycomes fromhttp://security.ubuntu.com/ubuntu/,Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg(fromubuntu-keyring, already on the boxes), with the same components.ansible_distribution_release. URIs, keyrings and components arecommon_apt_*role defaults.Update apt cache(whosecache_valid_time: 3600would skip a recent cache). It forces a refresh when the file changed or when the upstream security index is missing, so an interrupted run fixes itself on the next one. That refresh andUpdate apt cacheretry three times. Because apt only warns when one source fails and another answers, the role then checks that the upstream security index is really there, and stops the run on that box if it is not. A dry run shows the file's diff and changes nothing.roles/common/tasks/apt_lists_metric.yml, every box: two timestamps in/var/lib/node_exporter/textfile/apt-lists.prom, written by/usr/local/sbin/ipni-apt-lists-metric. Apt hooks run it after every package-list refresh, and the role runs it once per run (not in a dry run).node_apt_security_lists_timestamp_secondsis when the newest security index was published: theDate:of the newest*_dists_<release>-security_InRelease(or its mtime), 0 if there is none.node_apt_lists_refresh_timestamp_secondsis the last refresh apt reported as a success, from a stamp our ownPost-Invoke-Successhook touches (Debian has noupdate-success-stamp). apt runs that hook when any source answered, so this shows a refresh that stopped running or reaches nothing. It does not show one source failing; the security index metric does.time() - <metric>.The rendered file for noble differs from what the boxes have only in the security stanza's
URIsandSigned-By. The metric's logic, run read-only on the boxes, prints the age you'd expect from each one's lists.Trade-offs
security.ubuntu.comper box per day.site.ymlrun stops on the box where the refresh still fails after three tries, rather than carrying on without a security index.-updates, so a security update that needs a version only-updateshas stays pending until someone upgrades it by hand. That was already true before this change, and it is rare, since Ubuntu builds security updates against the release and security pockets.Alerting
This repo has no alert rules: monitoring lives in ipni/infra. The IPFS boxes' node-exporter doesn't read the textfile directory yet, the same gap
node_reboot_requiredhas. Two things are needed there:kubernetes/platform/node-exporter):--collector.textfile.directorywith a read-only host mount of/var/lib/node_exporter/textfile.rules/fleet.yaml):node_apt_security_lists_timestamp_secondsmore than a day behind another's (they all read the same upstream index): one box's source is stale or unreachable;time() - node_apt_lists_refresh_timestamp_secondsover a day or two: the refresh has stopped;time() - node_apt_security_lists_timestamp_secondsover a generous fixed age, a week say: the backstop for upstream being unreachable from every box at once. Tighten it once someone has watched how often theDate:changes, since a quiet archive makes it grow too.Interaction with #36
This builds on #36 (merged), which turned the nightly unattended runs back on. #36 makes them run; this makes sure they read a current index. Neither depends on the other being applied first.
Rollout
site.ymlfrom CI, one box at a time as usual. The real change on each Ubuntu box is the newubuntu.sources, oneapt-get update, a new apt hook and the metric script. Nothing is installed or restarted, and k3s and needle aren't touched.Checks
ansible-playbook --syntax-checkon all seven playbooks,yamllint .,ansible-lint(production profile, 0 failures). Nothing that renders manifests changed.🤖 Generated with Claude Code