Fix Ansible 'SSL: CERTIFICATE_VERIFY_FAILED' in uri and get_url Modules

intermediate🔧 Ansible2026-07-01| Ubuntu 20.04/22.04, RHEL/CentOS 8/9, Ansible 2.9–2.17, Python 3.8+

Error Message

Status code was -1 and not [200]: Request failed: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:1007)>
#ansible#ssl#uri#get_url

The Situation

Your playbook is downloading a file or polling an internal API via get_url or uri. Everything looks fine — until this lands in your terminal:

FAILED! => {
    "msg": "Status code was -1 and not [200]: Request failed: "
}

Status code -1 means the connection never got through. Common culprits:

  • Internal server running a self-signed certificate
  • Corporate SSL inspection proxy (Zscaler, Forcepoint, Squid with SSL bump)
  • Staging or dev environment with a cert signed by an internal CA Python doesn't know about
  • An expired server cert nobody caught — until Ansible did

Why Python/Ansible Rejects the Cert

Ansible's uri and get_url modules use Python's urllib internally. It validates SSL certificates against the system CA bundle — or the certifi package bundle if that's installed. When the server's cert isn't signed by a CA in that bundle, Python kills the connection and returns status -1. No prompt, no warning. Just a hard stop.

Browsers handle this differently — they ship their own trust store and show a click-through warning. Python doesn't offer that option.

Quick Fix — Disable Verification (Use With Caution)

Need it working right now on a controlled internal network? Add validate_certs: false:

- name: Download file from internal server
  get_url:
    url: "https://internal.example.com/files/package.tar.gz"
    dest: /tmp/package.tar.gz
    validate_certs: false
- name: Hit internal API
  uri:
    url: "https://internal-api.example.com/health"
    method: GET
    validate_certs: false
  register: api_response

This disables SSL verification entirely. Acceptable for internal tooling behind a firewall. Not acceptable for anything touching the public internet or sensitive data — you're wide open to MITM attacks.

Permanent Fix — Add Your CA Certificate

The correct approach is teaching Python to trust your internal CA. Three options, depending on your setup.

Option 1: Point Ansible to a Specific CA Bundle

The ca_path parameter lets you specify exactly which cert to trust, per task:

- name: Download with custom CA
  get_url:
    url: "https://internal.example.com/files/package.tar.gz"
    dest: /tmp/package.tar.gz
    ca_path: /etc/ssl/certs/internal-ca.crt

- name: URI with custom CA
  uri:
    url: "https://internal-api.example.com/status"
    ca_path: /etc/ssl/certs/internal-ca.crt

Get the CA cert from your infra or security team. For self-signed server certs, pull it directly from the control node:

# Grab the cert from the server
openssl s_client -connect internal.example.com:443 -showcerts </dev/null 2>/dev/null | \
  openssl x509 -outform PEM > /etc/ssl/certs/internal-ca.crt

Option 2: Add the CA to the System Trust Store

Add it once, and everything — Ansible, curl, Python scripts — picks it up automatically. Best choice for recurring use.

On Ubuntu/Debian:

sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates

On RHEL/CentOS/Rocky:

sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/internal-ca.crt
sudo update-ca-trust

Once done, drop validate_certs: false or ca_path from your tasks — the system trust store handles it from here.

Option 3: Set the CA Bundle via Environment Variable

No root access? Running in a container? Set REQUESTS_CA_BUNDLE in your playbook's environment block:

- name: Download with CA via env var
  get_url:
    url: "https://internal.example.com/files/package.tar.gz"
    dest: /tmp/package.tar.gz
  environment:
    REQUESTS_CA_BUNDLE: /path/to/internal-ca.crt

For a permanent setup, export these in the shell profile that launches Ansible (e.g. ~/.bashrc or /etc/profile.d/ansible-ca.sh):

export REQUESTS_CA_BUNDLE=/etc/ssl/certs/internal-ca.crt
export SSL_CERT_FILE=/etc/ssl/certs/internal-ca.crt

Corporate Proxy (SSL Inspection)

Zscaler and similar proxies intercept HTTPS traffic and re-sign every connection with the proxy's own CA. Your Python sees that proxy cert, doesn't trust it, and bails.

Fix: get the proxy's root CA from IT and add it using Option 2 above. Once that CA is in the trust store, all HTTPS through the proxy works — no verification bypass needed.

First, confirm a proxy is actually the culprit:

# Check what cert you're actually getting
openssl s_client -connect internal.example.com:443 </dev/null 2>/dev/null | openssl x509 -text -noout | grep -E "Issuer|Subject"

If Issuer shows your proxy vendor (something like Zscaler Inc) instead of the server's actual CA, that's your problem.

Verify the Fix

Test on the control node before running the full playbook:

# Quick Python check
python3 -c "
import urllib.request
response = urllib.request.urlopen('https://internal.example.com')
print(response.status)
"
# Or inspect the cert chain with curl
curl -v https://internal.example.com 2>&1 | grep -E "SSL|certificate|verify"

Then run a minimal playbook to confirm Ansible is happy:

- name: Verify SSL fix
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Check endpoint
      uri:
        url: "https://internal.example.com/health"
        method: GET
      register: result
    - debug:
        msg: "Status: {{ result.status }}"

Status: 200 without validate_certs: false — you're done.

Checklist

  • Get the actual CA cert — don't just disable verification
  • On Ubuntu: update-ca-certificates; on RHEL: update-ca-trust
  • Use ca_path for task-level control, system trust store for a global fix
  • Behind a corporate proxy? Get the proxy root CA from IT — not the server cert
  • Run openssl s_client first to see exactly what cert chain you're dealing with

Related Error Notes