Ansible 2.19.13 - URL Credential Masking


The Ansible project published ansible-core v2.19.13 on 8 September 2026 at 16:48 UTC. mask_url in lib/ansible/module_utils/urls.py now masks a URL that carries a password and no username, such as redis://:password@host. The same tag rejects tempfile prefix and suffix values whose basename differs from the raw string, which is how path separators were reaching mkstemp and mkdtemp.

The full release notes and downloads are on the GitHub release page. Itemized changes for this tag are in the v2.19 changelog.

mask_url is the helper this release adds for authentication data embedded in a URL. The minor changelog line is that addition. The bugfix beside it covers a password with an empty username. Before this tag, redis://:password@host was returned unmasked.

urlparse reports that username as an empty string and still sets the password. The helper rewrites userinfo to ****, so the example comes back as redis://****:****@host. A URL with both a username and a password uses the same ****:**** form. A username alone becomes ****. A URL with no userinfo is returned unchanged.

The mask stops at userinfo. Query parameters stay in the string. http://host/path?password=secret has no userinfo, so mask_url returns it unchanged. Secrets passed as token or password query keys still show up in module results and in verbose logs.

Plays that read the password back from a displayed URL lose that path. The value remains in the task argument or the vaulted variable that built the URL. Logs from runs on 2.19.12 and earlier can still hold the old userinfo. This tag leaves stored output alone.

Five callers now mask URI authentication on display and on return: apt_key, get_url, rpm_key, uri, and the url lookup. module_utils.urls masks inline URL authentication on errors.

fetch_url stores mask_url(url) on the info dict, including failures that start at status -1, and it masks r.geturl() after a successful fetch. fetch_file puts the masked URL in Failure downloading when fetch_url returns no response, the status is 400 or higher, or the download raises.

apt_key masks the URL in Failed to download key at %s: %s and in Error getting key id from url. rpm_key masks it in failed to fetch key at %s , error was: %s and in Not a public key. get_url masks the url field on success, on HTTP 304, and on Request failed. A missing checksum is reported as Unable to find a checksum for file '%s' in '%s', with checksum_url passed through mask_url.

uri builds one masked_url. HTTP errors use HTTP Error while fetching plus that value. A relative location header is joined onto the masked URL, so the returned location keeps the password out of the result.

The url lookup in lib/ansible/plugins/lookup/url.py masks the target in the vvvv line url lookup connecting to. Its AnsibleError strings, including Received HTTP error for and Error connecting to, append the masked URL and leave out the raw exception text. The cause stays chained with raise ... from e, so a verbose traceback can still show it. The one line task error stops at the masked URL.

unarchive rejects a src that contains :// when remote_src is false. The message is Unsupported option, an URI src (...) is only supported when remote_src is True, with the source masked. A play that passed a URL to unarchive under the default remote_src fails on v2.19.13.

lib/ansible/modules/tempfile.py rejects prefix and suffix values that contain path components. The module note says both options must be file name components and must not contain path separators.

The check is prefix != os.path.basename(prefix), and the same compare for suffix. On failure the message starts with prefix must be a valid file name component or the suffix form of that sentence, and the module includes the rejected value. It returns before mkstemp or mkdtemp. Both state=file and state=directory run the check. Integration tests cover prefix ../../nested and suffix ../world for each state.

Flat names still pass. The default prefix is ansible. and the default suffix is empty. A hostname, a run id, or .csv has no directory separator, so the basename matches.

The compare has a gap. os.path.basename("..") is .., so a bare .. is accepted. On a POSIX target a backslash is a filename character, so a prefix such as a\b is accepted too. Strings with a separator fail: foo/bar, ../x, /tmp/x, and a trailing slash. The module has no config key to disable the check. Callers that passed a directory in prefix or suffix have to reduce that argument to one name fragment.

The GitHub release page lists two artifacts.

The built distribution is ansible_core-2.19.13-py3-none-any.whl, 2426742 bytes, SHA256 d4a256d0891b3cb31ddbc9ff8a7bf6fd359379007799c54390b66b8f94fed3d3.

The source distribution is ansible_core-2.19.13.tar.gz, 3432103 bytes, SHA256 3711fc5db7a265f93b34cf3f0db8b9708fb0eec12211a6be32ed6edd6c68044b.

The wheel tag py3-none-any is a pure Python build, so a controller image that vendors ansible-core can swap the file without an ABI rebuild. Pin the digest if the image build should fail when the bytes change. Retest roles that surface URLs from get_url, uri, apt_key, rpm_key, or the url lookup, roles that pass a URL to unarchive with remote_src left false, and roles that build a tempfile prefix or suffix from a path.