DevOps · Infrastructure Operations

Conffile Conflicts During Package Upgrades

How dpkg and rpm decide whether a configuration file has been edited locally, why neither will merge, and how unattended upgrades end up keeping stale configuration.

Most of what a package upgrade does is replace files. The package manager knows which paths the old version owned, removes them, unpacks the new version over the same paths, and runs the maintainer scripts. Nothing is negotiated, because the package is the only writer the model admits. Configuration files are the exception. For those, the file on disk is expected to differ from the file in the package, and the difference is somebody’s deliberate decision. Both dpkg and rpm therefore carry a separate mechanism for them, and both mechanisms are heuristics rather than merges.

The heuristic needs to answer one question at each upgrade: has this file been edited since the package last wrote it. Everything that follows, including the prompt that blocks an unattended upgrade at three in the morning, is a consequence of how that question is answered.

The checksum recorded at install time

dpkg’s state lives under /var/lib/dpkg. The status file holds one stanza per installed package, and for packages that ship configuration files the stanza contains a Conffiles field listing each path together with the MD5 digest of the version the package last distributed. The digest is 128 bits, written as 32 hexadecimal characters. A path is a conffile only because the package declared it as one in its control metadata; being located under /etc is a convention, not the rule.

The declaration matters because it changes the ownership rules. Ordinary files belong to the package and are replaced unconditionally. Conffiles belong to the administrator, and the package only proposes new contents for them.

The database is queryable without reading the raw file:

dpkg-query -W -f='${Conffiles}\n' openssh-server
#  /etc/ssh/sshd_config <32 hex digits>

At upgrade time dpkg has three values available: the digest recorded in the database, the digest of the file currently on disk, and the digest of the file in the new package. Comparing them yields four cases, and the case determines whether anything is written, whether the administrator is asked, and under what name the losing version is preserved.

Recorded vs diskRecorded vs new packageAction
equal (unmodified)equal (package unchanged)nothing to do
equal (unmodified)differentinstall the new file silently
different (modified)equal (package unchanged)keep the local file, no prompt
different (modified)differentprompt; one side is saved aside

The fourth case is the conflict. dpkg offers to keep the old file, install the new one, show a diff, or drop into a shell. Whichever side loses is written next to the original with a suffix: .dpkg-dist for the newly shipped version, .dpkg-old for the local version. The suffixes carry no semantics beyond provenance, and nothing collects them afterwards; a machine upgraded for a decade accumulates them in /etc as a fossil record of every decision made under pressure.

rpm reaches a similar place by a different route. A file marked %config is a configuration file; a file marked %config(noreplace) is one that must not be replaced when it has been edited. For %config, rpm installs the new file and saves the local one as .rpmsave. For %config(noreplace), rpm keeps the local file and saves the new one as .rpmnew. The default is the dangerous one: an unmarked or plainly %config file is replaced, and the local edit survives only as a .rpmsave that no daemon reads. The asymmetry between the two package managers is worth remembering, because a procedure written for one produces the opposite outcome on the other.

Why the package manager will not merge

All four outcomes above are decided by comparing whole files. Nothing looks inside them. That is a deliberate limit, and the reason is that a merge requires knowing what the file means.

A three-way merge needs a common ancestor, the local version, and the incoming version. dpkg retains the ancestor only as a digest, not as content, so the base text is unavailable unless the administrator happened to keep a .dpkg-old. Without a base, a two-way diff cannot distinguish a line the administrator added from a line the package deleted. The result of guessing is a file that parses and is wrong: a directive that changed meaning between versions, an option whose default flipped, a stanza that is now required and is absent because the merge kept the local block wholesale. The package manager has no schema for the file and no way to test the result, so it declines.

The ucf utility, used by some Debian packages for files the package generates rather than ships, does keep a copy of each registered version and performs a real three-way merge. That works because it owns the whole history of the file. A generic package manager cannot afford to keep every historical version of every conffile, and could not merge files it does not understand even if it did.

A file that disappears from the package’s conffile list is a related edge case. dpkg leaves it on disk and records it as obsolete; removal happens at purge. The service may keep reading a configuration file that the current package version no longer knows about, which produces behaviour that no installed package’s documentation describes.

When the prompt never appears

The conflict prompt needs a terminal and a human. Neither exists during an unattended upgrade, so the frontend supplies a policy instead. The apt noninteractive frontend passes --force-confdef and --force-confold to dpkg, which resolves every conflict by keeping the local file and discarding the new one. Nothing fails. The upgrade reports success.

The damage is deferred and indirect. A new package version may require a configuration key that did not exist before, or may have changed the type of an existing value. The daemon starts with the old file, fails to parse it, and exits; or worse, starts with defaults for the new keys and silently behaves differently from the configuration the administrator believes is in effect. The .dpkg-dist file sitting beside the live one is the only evidence, and it is invisible to anything that does not list the directory.

The same policy explains a class of upgrade failures that look like packaging bugs. A package that adds a required directive in a new upstream release has no way to make an unattended system adopt it without violating the conffile contract. Some packages work around this by shipping the file as a non-conffile generated at postinst time, which moves the problem into maintainer script logic and out of the administrator’s control entirely.

Where the model stops working

The conffile mechanism assumes the package is the only thing that writes the file. Configuration management breaks that assumption. When a system management tool rewrites /etc/ssh/sshd_config from a template after every package operation, the digest in dpkg’s database no longer matches the disk, and every subsequent upgrade classifies the file as locally modified. The prompt returns, unattended upgrades pin the file, and the tool overwrites it again. The mechanism is not wrong; it is being asked to track edits whose author it cannot see.

The second assumption is that configuration lives in files the package owns. Options passed on a command line, values in a database, environment files referenced by a unit, and settings compiled into an image all sit outside the model. A package can be fully upgraded, fully consistent with its conffiles, and still be running with configuration that no longer matches the software.

Immutable images remove the mechanism entirely rather than working around it. When a machine’s filesystem is built once and never modified, a package upgrade is a rebuild, and the conffile prompt cannot occur because there is no terminal, no administrator, and no persistent local edit to preserve. The preservation problem reappears one level up: an edit made to a running container is lost at the next replacement, and an edit made to the build recipe is applied to every machine at once, including the ones whose local variation was the reason for the edit. The package manager’s four cases collapse into a single case, and the judgement it used to defer to a human is now made by whoever reviews the change to the recipe.

None of this is a defect that a better package manager would fix. Deciding whether a text edit is still valid requires understanding the program that reads the file, and that understanding does not exist at the packaging layer. The mechanism is a way of preserving an administrator’s intent across an operation that would otherwise destroy it, and it is exactly as reliable as the digest comparison that implements it. Understanding where it stops is what separates a clean upgrade from a service that starts with half of its old configuration and half of its new defaults.

The dependencies that decide whether the upgrade can proceed at all are a separate problem, covered in dependency resolution and partial upgrades; the question of when the new binaries take effect is covered in reboot-required and live kernel patching.

References

  1. Debian Policy Manual, Chapter 10: Files — Debian Project
  2. dpkg(1) manual page — man-pages project
  3. Maximum RPM — Edward C. Bailey, Red Hat Press