ansible-core 2.22 Adds Native Secret Masking to Pipelines

ansible-core 2.22 introduces non-destructive secret masking with $REDACTED$, updates controller Python support to 3.15, and deprecates legacy sanitizers.

On November 2, 2026, the Ansible engineering team released ansible-core 2.22.0 on GitHub and published corresponding artifacts on PyPI. The update brings a structural change to how Python automation jobs handle sensitive information. In previous versions, engineers managing automated provisioning pipelines had to treat secrecy as an all-or-nothing proposition. Setting no_log: true on a task hid the entire task result from console output and log files. When an infrastructure task failed inside an automated continuous integration runner, operators could not see standard error output or tracebacks. If someone removed no_log: true to troubleshoot the failure, or enabled verbose flags such as -v or -vvv, plain passwords, API tokens, and private keys were written directly into build logs.

With ansible-core 2.22, the execution engine introduces a native secret masking subsystem that tracks sensitive values and substitutes them with $REDACTED$ in logs, standard output, and callback streams. The masking operates without modifying variables in memory, which means tasks, templates, and conditional checks continue to receive raw credentials while human-readable displays remain sanitized. Alongside this security feature, the release drops Python 3.12 support for the control node, adds Python 3.15 support, and establishes a strict deprecation timeline for legacy sanitization helpers and implicit failure handling.

For teams running infrastructure automation in scheduled runners and continuous integration environments, these changes alter how playbooks should be written and debugged. Credential leakage in automation logs is a persistent operational hazard, similar to the logging problems previously documented in /posts/sagemaker-git-utils-logs-not-a-secret-2026. The new release provides a built-in defense mechanism that replaces ad-hoc shell wrappers and custom masking scripts.

The persistent failure modes of no_log in CI runners

Automation pipelines running on platforms such as Jenkins, GitHub Actions, and GitLab CI rely heavily on stdout and stderr to report job progress. When an Ansible task provisions a database, configures an identity provider, or requests an authentication token, the task parameters often contain confidential strings. For years, Ansible provided no_log: true as the primary defense against logging sensitive data.

The problem with no_log: true was its binary behavior. When enabled, Ansible suppressed the entire result dictionary of the task. If a provisioning script failed because of a network timeout or a syntax error in a configuration template, the CI console printed only a generic failure message. The actual error output, stderr trace, and exit codes were discarded. Engineers attempting to fix broken deployments frequently toggled no_log: false in a temporary commit, ran the pipeline again, and exposed credentials to anyone with read access to the CI build history.

A second limitation appeared when playbooks passed data between tasks using register. If a task fetched an authentication token and stored the output in a registered variable, any subsequent task that referenced that variable could accidentally print the token if it omitted no_log: true. A debug task, a failure notification hook, or an unhandled exception in a downstream role would expose the secret string.

Furthermore, custom modules and action plugins often attempted to sanitize their own outputs using custom regular expressions or string replacement functions. These approaches frequently missed edge cases, such as URL-encoded tokens, multi-line private keys, or passwords embedded within nested dictionaries. The lack of a centralized, engine-level secret registry meant that each module author had to reinvent redaction logic, leading to inconsistent behavior across different collections.

How native secret masking works in ansible-core 2.22

The official ansible-core 2.22 Porting Guide and the user guide on Masking secrets in Ansible output describe how the new secret masking architecture resolves these historical pain points. Instead of suppressing entire task results, the engine tracks specific sensitive values in a process-wide registry and intercepts them at egress boundaries.

Masking in ansible-core 2.22 is non-destructive. During playbook execution, variables stored in memory retain their exact values. When a task passes a database password to a module, evaluates a Jinja expression, or renders a template file on a remote host, Python processes the original plaintext string. The masking layer activates only when information is about to be rendered to external output destinations:

  1. The controller display object, which writes output to terminal screens, standard output, and standard error streams.
  2. File logging targets configured through the log_path configuration directive in ansible.cfg.
  3. Event payloads passed down to Ansible callback plugins, including custom reporting tools and notification integrations.
  4. Remote module logging facilities operating on managed nodes.

Whenever any text heading toward these output channels contains a registered secret, Ansible replaces that substring with $REDACTED$. Because the redaction happens at the egress boundaries, task result dictionaries still show execution status, changed flags, execution time, and non-sensitive stdout or stderr lines. If a task fails, operators can read the diagnostic error messages while the specific credential string remains shielded.

Ansible-core 2.22 automatically registers sensitive values from multiple standard sources without requiring playbook modifications:

  • Decrypted Ansible Vault variables and values loaded from encrypted vault files.
  • Module options that are marked with no_log: true in their official argument specifications.
  • Input collected through interactive vars_prompt definitions when configured with private: true.
  • Passwords supplied via command line flags, such as --ask-pass, --ask-become-pass, and --ask-vault-pass.
  • Standard connection and privilege escalation variables, including ansible_password, ansible_ssh_pass, and ansible_become_password.
  • Secrets retrieved through built-in lookup plugins, such as the password and unvault lookups.

By registering these values automatically, the engine protects default credential flows across existing playbooks without requiring maintainers to rewrite their variable declarations.

Registering custom tokens with new Jinja filters

While automatic registration covers built-in credentials and vault variables, automation workflows frequently generate or retrieve ephemeral secrets dynamically. For example, a playbook might invoke an external REST API using ansible.builtin.uri, receive a short-lived JSON Web Token, and use that token to configure cloud infrastructure in later steps. Because this token was returned dynamically during execution, Ansible would not recognize it as a secret by default.

To solve this problem, ansible-core 2.22 introduces two new built-in filter plugins in the ansible.builtin namespace: register_secret and mask_secrets.

The register_secret filter takes a string, registers it in Ansible’s internal secret manager, and returns the original string unaltered. This allows engineers to flag dynamic values as confidential at the moment of creation while immediately assigning them to variables or facts.

Consider an automation task that queries an internal authorization service:

- name: Request deployment token from internal auth service
  ansible.builtin.uri:
    url: "https://auth.internal.corp/v1/tokens"
    method: POST
    body_format: json
    body:
      service: "deployer"
  register: auth_response

- name: Store and register the returned bearer token
  ansible.builtin.set_fact:
    deploy_token: "{{ auth_response.json.token | ansible.builtin.register_secret }}"

- name: Execute deployment using registered token
  ansible.builtin.command:
    cmd: "deployctl apply --token {{ deploy_token }} --env production"
  register: deploy_result

In this workflow, the second task extracts the token and feeds it to register_secret. From that point forward, any appearance of that token string across any task output, callback hook, or log line will be replaced with $REDACTED$. Even if the third task fails and prints its command invocation to stderr, the actual token value will not appear in the CI runner console.

The companion filter, mask_secrets, performs explicit string sanitation. It accepts a block of text and scans it against the secret registry, returning a copy of the text with all registered secrets replaced by $REDACTED$. This filter is useful when an automation task prepares notification messages, Slack webhooks, or diagnostic summaries that must be pre-sanitized before transmission to external services.

For collection maintainers and custom plugin authors, ansible-core 2.22 provides the ansible.module_utils.secrets Python module. Action plugins and custom modules can import this module to register sensitive values directly within Python code:

from ansible.module_utils.secrets import register_secret

def run_custom_auth(connection, credential_string):
    token = connection.generate_session_token(credential_string)
    register_secret(token)
    return token

Using this programmatic API, plugin authors avoid maintaining custom regex replacement dictionaries, delegating all redaction to the core engine.

Upgrading controller environments to ansible-core 2.22

Upgrading an automation fleet to ansible-core 2.22 involves adjusting Python runtime requirements on the control node. The release notes establish new version boundaries that affect CI runner container images, deployment bastions, and scheduler virtual environments.

On the control node (the machine where ansible-playbook runs), support for Python 3.12 has been dropped. Support for Python 3.15 has been officially added. Control nodes running ansible-core 2.22 must now operate on Python 3.13, 3.14, or 3.15. Attempting to install or run the package on a Python 3.12 interpreter will trigger installation failures or immediate runtime errors.

For target nodes (the managed hosts where Ansible executes modules), compatibility remains broader. Ansible-core 2.22 supports target systems running Python 3.9 through Python 3.15. The Ansible engineering team originally evaluated dropping Python 3.9 target support in this release, but postponed that change to ansible-core 2.23. This gives operations teams additional runway to modernize older managed servers while upgrading their control plane immediately.

This controller runtime requirement has direct implications for containerized CI/CD pipelines. Teams that run Ansible inside standard base images such as Debian Bookworm or Ubuntu 22.04 LTS often rely on system Python versions that predate Python 3.13. Upgrading to ansible-core 2.22 requires updating runner Dockerfiles to modern distribution bases, using deadsnakes PPAs, or adopting standalone Python runtimes.

These interpreter updates follow broader ecosystem transitions, such as the standard library archive handling updates discussed in /posts/python-3152-tar-filter-cron-box-2026. When building container images for automation controllers, utilizing fast package managers like uv can streamline interpreter and dependency installation, as detailed in /posts/uv-01218-path-traversal-pip-check-2026.

A practical Dockerfile snippet for an updated automation runner demonstrates the required environment configuration:

FROM python:3.13-slim

ENV PYTHONUNBUFFERED=1 \
    DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        git \
        openssh-client \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir ansible-core==2.22.0

WORKDIR /workspace
ENTRYPOINT ["ansible-playbook"]

Pinning ansible-core==2.22.0 on Python 3.13 or 3.14 provides a stable controller foundation that avoids compatibility breaks with existing system packages.

Deprecations: legacy sanitizers and non-zero return codes

In addition to new features, ansible-core 2.22 introduces important deprecations that prepare the codebase for future major releases. Teams maintaining internal modules, action plugins, or custom collections must review their code to prevent unexpected breakage in upcoming versions.

First, the core team has deprecated older manual sanitization functions in ansible.module_utils. Specifically:

  • ansible.module_utils.basic.heuristic_log_sanitize() is deprecated.
  • ansible.module_utils.common.parameters.remove_values() is deprecated.
  • ansible.module_utils.common.parameters.sanitize_keys() is deprecated.

These legacy helpers relied on heuristic string manipulation and dictionary traversal. They often produced incomplete results or caused performance degradation when processing deep data structures. The porting guide states that all three functions will be permanently removed in ansible-core 2.25. Maintainers should transition their code to use the new ansible.module_utils.secrets API instead.

Second, the live argument of ansible.utils.cmd_functions.run_cmd() is deprecated and scheduled for removal in ansible-core 2.25. Previously, passing live=True allowed commands to stream their output directly to stdout in real time without buffering. However, unbuffered character streaming bypasses the secret masking engine, creating a risk that sensitive strings could slip into terminal streams before matching against the registry. To preserve masking guarantees, commands must run through buffered handlers.

Third, ansible-core 2.22 deprecates automatic failure inference from non-zero return codes (rc). Historically, if a module or action plugin returned a dictionary containing {"rc": 1} without explicitly including "failed": True, the Ansible engine automatically assumed that the task had failed.

As documented in the ansible-core 2.22 Porting Guide, ansible-core 2.22 emits a runtime deprecation warning whenever a module or action plugin returns a non-zero exit code without setting an explicit failed key. This automatic failure inference will be completely eliminated in ansible-core 2.24. Once version 2.24 arrives, if a module returns rc: 1 without "failed": True or without calling fail_json(), Ansible will treat the task as successful. This change enforces clean contract boundaries between module outputs and engine control flow, but requires audit work for legacy in-house modules.

Maintainer checklist for upgrading automation pipelines to ansible-core 2.22

Migrating production automation workflows to ansible-core 2.22 requires systematic validation across infrastructure controllers, shared roles, and CI definitions. Operations teams can follow this checklist to verify their environments:

  1. Verify controller Python interpreter versions across all runner environments, deployment containers, and scheduled cron nodes. Ensure that the Python environment executing ansible-playbook runs Python 3.13, 3.14, or 3.15, and remove Python 3.12 from controller build matrices.
  2. Scan custom plugins for deprecated sanitization calls. Search private collections and module libraries for references to heuristic_log_sanitize, remove_values, or sanitize_keys, and replace these calls with imports from ansible.module_utils.secrets.
  3. Remove live streaming parameters from command execution. Check custom action plugins and connection helpers for run_cmd(..., live=True), and switch to standard buffered execution calls to maintain full coverage under the secret masking engine.
  4. Audit module return dictionaries for explicit failure keys. Inspect internal modules that execute shell commands or CLI tools, and ensure every failure path explicitly sets failed=True or invokes module.fail_json(msg=...). Avoid relying on returning an exit code alone.
  5. Apply register_secret on dynamic credential tasks. Identify playbook tasks that fetch temporary bearer tokens, database passwords, or cloud credentials, and attach the | ansible.builtin.register_secret filter when setting facts or assigning registered outputs.
  6. Evaluate task-level no_log directives. Review existing tasks that use no_log: true solely to prevent credential leakage. Test running these tasks with native secret masking enabled; restoring diagnostic output while redacting secrets simplifies troubleshooting when infrastructure tasks encounter network or permission issues.
  7. Test callback and log forwarding integrations. When an automation pipeline sends task events to external SIEM tools, Elasticsearch, or Slack, confirm that your callback plugins receive sanitized strings containing $REDACTED$ instead of raw tokens.

By completing these verification steps, operations teams can adopt ansible-core 2.22 to improve pipeline security and debugging visibility without introducing unexpected runtime failures.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.