The problem persists with Ubuntu 22.04 LTS (tested with one of the final RCs: https://iso.qa.ubuntu.com/qatracker/milestones/432/builds/247146/downloads). I did some analysis and the issue seems to be the "$MIRROR/$RELEASE" part of the custom template. The relevant source code part (the generate_sources_list function in curtin/commands/apt_config.py): ``` 333 def generate_sources_list(cfg, release, mirrors, target=None, arch=None): 334 """ generate_sources_list 335 create a source.list file based on a custom or default template 336 by replacing mirrors and release in the template 337 """ 338 aptsrc = "/etc/apt/sources.list" 339 340 tmpl = cfg.get('sources_list', None) 341 from_file = False 342 if tmpl is None: 343 LOG.info("No custom template provided, fall back to modify" 344 "mirrors in %s on the target system", aptsrc) 345 tmpl = util.load_file(paths.target_path(target, aptsrc)) 346 from_file = True 347 348 entries = [SourceEntry(line) for line in tmpl.splitlines(True)] 349 if from_file: 350 # when loading from an existing file, we also replace default 351 # URIs with configured mirrors 352 entries = update_default_mirrors(entries, mirrors, target, arch) 353 354 entries = update_mirrors(entries, mirrors) 355 entries = update_dist(entries, release) ``` After SourceEntry from python-apt ("from aptsources.sourceslist import SourceEntry") parses the custom template (s. apt.sources_list above), the data ("entries") looks as follows: ``` {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE', 'dist': '$RELEASE', 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE $RELEASE main restricted\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE-updates', 'dist': '$RELEASE-updates', 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE-updates $RELEASE-updates main restricted\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE-security', 'dist': '$RELEASE-security', 'comps': ['main', 'restricted'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE-security $RELEASE-security main restricted\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': True, 'disabled': False, 'type': '', 'architectures': [], 'trusted': None, 'uri': '', 'dist': '', 'comps': [], 'comment': '', 'line': '\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE', 'dist': '$RELEASE', 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE $RELEASE universe\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE-updates', 'dist': '$RELEASE-updates', 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE-updates $RELEASE-updates universe\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} {'invalid': False, 'disabled': False, 'type': 'deb', 'architectures': [], 'trusted': None, 'uri': '$MIRROR/$RELEASE-security', 'dist': '$RELEASE-security', 'comps': ['universe'], 'comment': '', 'line': 'deb $MIRROR/$RELEASE-security $RELEASE-security universe\n', 'file': '/etc/apt/sources.list', 'template': None, 'children': []} ``` The problem here is that $RELEASE appears in the "uri" attribute, e.g.: "'uri': '$MIRROR/$RELEASE'". This causes the update_mirrors function (curtin/commands/apt_config.py) to fail: ``` 251 def update_mirrors(entries, mirrors): 252 """perform template replacement of mirror placeholders with configured 253 values""" 254 for entry in entries: 255 entry.uri = util.render_string(entry.uri, mirrors) 256 return entries ``` The "mirrors" parameter has the following value: "{'PRIMARY': 'http://ubuntu.company.tld', 'SECURITY': 'http://ubuntu.company.tld', 'MIRROR': 'http://ubuntu.company.tld'}" Therefore, it fails with "KeyError: 'RELEASE'". The $RELEASE key is normally handled by the update_dist function but it only operates on entry.dist and not entry.uri. The easiest (but not necessarily most elegant) solution would probably be to put the "RELEASE" key in the "mirrors" dictionary as well.