# Custom CA Certificates (Python)

LLMS index: [llms.txt](/llms.txt) | Full content: [llms-full.txt](/llms-full.txt)

---

## Overview

You can configure Python container images to trust custom Certificate Authority (CA) certificates for TLS connections. Python libraries have different trust store behaviors compared to other [OpenSSL-based images](custom-ca-openssl.md):

- **[urllib](https://docs.python.org/3/library/urllib.request.html)** (built-in) reads CAs from *both* of these locations:
  1. `/etc/pki/tls/cert.pem` - the main CA bundle file
  2. `/etc/pki/tls/certs/` - directory with hashed certificate symlinks
- **[requests](https://pypi.org/project/requests/)** (third-party) ignores `/etc/pki` entirely and requires the `REQUESTS_CA_BUNDLE` environment variable to use system CAs

**Note**: The `requests` library is not included in the standard Python image. To use it, you need to build a derived image:

```dockerfile
FROM quay.io/hummingbird/python:latest
RUN ["pip3", "install", "requests"]
```

```bash
podman build -t localhost/my-python-with-requests .
```

There are two approaches for custom CAs depending on your needs:

1. **Custom bundle file** - Replace system CAs entirely with your own bundle
2. **Add custom CAs** - Merge your custom CAs with the system defaults

## Approach 1: Custom Bundle File

Use this when you want to trust **only** your custom CAs and block all default CAs. This is a common case with [OpenShift's custom PKI bundle][os-custom-pki].

Mount your CA bundle to `/etc/pki/tls/cert.pem`, set `$REQUESTS_CA_BUNDLE` to that, and block the hashed directory with an empty directory:

### Custom bundle with Podman

```bash
podman run --rm \
  -v /path/to/ca.crt:/etc/pki/tls/cert.pem:ro,Z \
  --tmpfs /etc/pki/tls/certs:notmpcopyup \
  -e REQUESTS_CA_BUNDLE=/etc/pki/tls/cert.pem \
  localhost/my-python-with-requests python3 -c "
import urllib.request
print(urllib.request.urlopen('https://your-server/').status)
import requests
print(requests.get('https://your-server/').status_code)
"
```

**Note:** Podman requires the `:notmpcopyup` option as mounting tmpfs behaves like an overlay. Docker's `tmpfs` creates an empty directory by default, and does not know that option, so drop that option with Docker.

### Custom bundle with Kubernetes

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-ca-bundle
data:
  cert.pem: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
    ... more certificates if needed ...
---
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    # Use quay.io/hummingbird/python for urllib only
    # Use your derived image with requests installed for both libraries
    image: registry.example.com/my-python-with-requests:latest
    env:
    - name: REQUESTS_CA_BUNDLE
      value: /etc/pki/tls/cert.pem
    volumeMounts:
    - name: custom-ca
      mountPath: /etc/pki/tls/cert.pem
      subPath: cert.pem
      readOnly: true
    - name: empty-certs
      mountPath: /etc/pki/tls/certs
      readOnly: true
  volumes:
  - name: custom-ca
    configMap:
      name: custom-ca-bundle
  - name: empty-certs
    emptyDir: {}
```

### Custom bundle on OpenShift with CA injection

On OpenShift, the cluster admin may have already added your organization's CA certificates to the cluster-wide trust store. You can use OpenShift's automatic CA injection feature to make these certificates available to your pods.

Create a ConfigMap with the `config.openshift.io/inject-trusted-cabundle=true` label. OpenShift will automatically populate it with the cluster CA bundle as a key named `ca-bundle.crt`:

```yaml
# OpenShift will inject a "ca-bundle.crt" into this automatically
apiVersion: v1
kind: ConfigMap
metadata:
  name: trusted-ca
  labels:
    config.openshift.io/inject-trusted-cabundle: "true"
---
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    # Use quay.io/hummingbird/python for urllib only
    # Use your derived image with requests installed for both libraries
    image: registry.example.com/my-python-with-requests:latest
    env:
    - name: REQUESTS_CA_BUNDLE
      value: /etc/pki/tls/cert.pem
    volumeMounts:
    - name: trusted-ca
      mountPath: /etc/pki/tls/cert.pem
      subPath: ca-bundle.crt
      readOnly: true
    - name: empty-certs
      mountPath: /etc/pki/tls/certs
      readOnly: true
  volumes:
  - name: trusted-ca
    configMap:
      name: trusted-ca
  - name: empty-certs
    emptyDir: {}
```

The key detail is using `subPath: ca-bundle.crt` to mount OpenShift's injected file to the correct location (`/etc/pki/tls/cert.pem`) that Python expects.

See the [Configuring a custom PKI][os-custom-pki] OpenShift documentation for details on cluster-wide CA configuration.

[os-custom-pki]: https://docs.redhat.com/en/documentation/openshift_container_platform/4.21/html/configuring_network_settings/configuring-a-custom-pki

## Approach 2: Add Custom CAs

Use this when you want to **merge** your custom CAs with the system defaults.

Mount your CA certificate as an individual file in `/etc/pki/tls/certs/` with a properly hashed filename. The filename must be the output of `openssl x509 -noout -subject_hash -in ca.crt` followed by `.0`.
Also set `$REQUESTS_CA_BUNDLE` to that directory:

### Custom CA file with Podman

```bash
# Generate the hashed filename
CA_HASH=$(openssl x509 -noout -subject_hash -in ca.crt)
echo "Hashed filename: ${CA_HASH}.0"

# Mount the CA certificate as an individual file in the certs directory
podman run --rm \
  -v ./ca.crt:/etc/pki/tls/certs/${CA_HASH}.0:ro,Z \
  -e REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ \
  localhost/my-python-with-requests python3 -c "
import urllib.request
print(urllib.request.urlopen('https://your-server/').status)
print(urllib.request.urlopen('https://google.com/').status)  # Default CAs still work
import requests
print(requests.get('https://your-server/').status_code)
print(requests.get('https://google.com/').status_code)  # Default CAs still work
"
```

### Custom CA file with Kubernetes

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-ca
data:
  ca.crt: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
---
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    # Use quay.io/hummingbird/python for urllib only
    # Use your derived image with requests installed for both libraries
    image: registry.example.com/my-python-with-requests:latest
    env:
    - name: REQUESTS_CA_BUNDLE
      value: /etc/pki/tls/certs/
    volumeMounts:
    - name: custom-ca
      # REQUIRED: replace example hash with: `openssl x509 -noout -subject_hash -in ca.crt` and append .0
      mountPath: /etc/pki/tls/certs/12abc456.0
      subPath: ca.crt
      readOnly: true
  volumes:
  - name: custom-ca
    configMap:
      name: custom-ca
```
