apt_key deprecated in Debian/Ubuntu - how to fix in Ansible

2023 Update: Ansible now has the ansible.builtin.deb822_repository module, which can add keys and repositories in one task. It's a little more complex than the old way, and requires Ansible 2.15 or later. See some common deb822_repository examples here, for example, the Jenkins tasks below can be consolidated (though the structure of the templated vars would need reworking):

- name: Add Jenkins repo using key from URL.
  ansible.builtin.deb822_repository:
    name: jenkins
    types: [deb]
    uris: "https://pkg.jenkins.io/debian-stable"
    components: [binary]
    signed_by: https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
    state: present
    enabled: true

For many packages, like Elasticsearch, Docker, or Jenkins, you need to install a trusted GPG key on your system before you can install from the official package repository.

Traditionally, you'd run a command like:

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

But if you do that in modern versions of Debian or Ubuntu, you get the following warning:

Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).

This way of adding apt keys still works for now (in mid-2022), but will stop working in the next major releases of Ubuntu and Debian (and derivatives). So it's better to stop the usage now. In Ansible, you would typically use the ansible.builtin.apt_key module, but even that module has the following deprecation warning:

The apt-key command has been deprecated and suggests to ‘manage keyring files in trusted.gpg.d instead’. See the Debian wiki for details. This module is kept for backwards compatiblity for systems that still use apt-key as the main way to manage apt repository keys.

So traditionally, I would use a task like the following in my Ansible roles and playbooks:

- name: Add Jenkins apt repository key.
  ansible.builtin.apt_key:
    url: https://pkg.jenkins.io/debian-stable/jenkins.io.key
    state: present

- name: Add Jenkins apt repository.
  ansible.builtin.apt_repository:
    repo: "deb https://pkg.jenkins.io/debian-stable binary/"
    state: present

The new way to do this without adding an extra gpg --dearmor task is to use get_url to download the file into the trusted.gpg.d folder with the .asc filename. Therefore the first task above can be replaced with:

- name: Add Jenkins apt repository key.
  ansible.builtin.get_url:
    url: "{{ jenkins_repo_key_url }}"
    dest: /etc/apt/trusted.gpg.d/jenkins.asc
    mode: '0644'
    force: true

See this issue in ansible/ansible for a little more background.

Comments

Just a quick note: keys in `/etc/apt/trusted.gpg.d` are trusted across all repos. The recommended approach is to put the key in `/usr/share/keyrings` and have the repo refer to it using the `signed-by` option.

If the key is in it's binary form , it needs to be with .gpg extension and we don't have to dearmor it

In Ansible, a command like that requires a lot of extra work to make idempotent, and it's not fun breaking idempotence :)

When you're doing it one-off, it's not a problem.

As an example/ echo Indrek and aduzsardi:

- name: Add gh cli signing key
get_url:
url: "https://cli.github.com/packages/githubcli-archive-keyring.gpg"
dest: /usr/share/keyrings/githubcli-archive-keyring.gpg
mode: 0644
force: true

- name: Add gh cli repo
apt_repository:
repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"
filename: github_cli
state: present
update_cache: True

- name: Install gh cli
apt:
name: gh
state: present

What if the url sends 404 but not as http header?
with get_url and force you will break the local key

And how do I verify the apt_key.id? Without that it is like doing a Bungee Jump without rope.

The new method is the builtin.deb822_repository module.
Works very good, but needs cleanup from old configuration if a packet source was previously configured via apt_key and apt_repository.

Note that usage of the deb822_repository module requires the python3-debian package. This can be installed in a virtual environment with pip install python-debian.

Here is my working example of installing Ondrej Sury's PHP PPA as closely as the system would when using add-apt-respository:

- name: Manage PHP PPA repository (deb822_repository)
  become: true
  ansible.builtin.deb822_repository:
    state: present
    name: "ondrej-ubuntu-php-{{ansible_distribution_release}}"
    types: [deb]
    uris: [https://ppa.launchpadcontent.net/ondrej/php/ubuntu]
    suites: ["{{ ansible_facts['distribution_release'] }}"]
    components: [main]
    signed_by: |
      -----BEGIN PGP PUBLIC KEY BLOCK-----
      .
      mQINBGYo0vEBEAC0Semxy5I2b8exRUxJfTKkHR4f5uyS0dTd9vYgMI5T3gsa7ypH
      HtE+GiZC+T9m/F9h66+XJMxhuNsKRs7T2In5NSeso9H/ytlSTayUaBtCFfRp6y6b
      6ozuRBfqYJGxhjAnIzvNF/Wpp2BvfQm3OrQ7uJJrt5IvzLDC4jPxl/Xs3sTT+Hbk
      bkKKprZ3xmy2enuwBaNWR/CUtAz3hbkzL1kGbhX9m3QidFJagVVdDw3aNEwo8ush
      djWfF+BajNvpDFYJKBGQbCeagB753Baa5yIN62x+THLnLiKTMDS1e7U0ZDiV9671
      noTbtN5TeZeyfsEmeZ8X60x11JIP3yYHYZT70/DyTYX3WC9yQFyIgVOfRlGklMKI
      k3TLMmtq8w5Hz1vovwzV7PzaQnmY+uNP2ZbAP4fJ3iFAj0L+u0i1nOFgTy0Lq058
      O/FjRrQxuceDDCF+9ThspXMw3Puvz8giuBDCdEda84uC7XWMdqgz/maLfFQjAmyP
      Ixi1EMxMlHYyZajpR1cdCfrAIQlnQjHSWmyeCFgXPPfRA71aCcJ7oSrDjogW6Ahd
      HRkQRKf1FF9BFzycgSQotfR+7CKfPQh1kghufM9W/spARzA709nGZjXJzgEJLQd3
      CDB6dIIxT/0YI36h3Qgfmiiw4twO24MMEqEEPIELz2WJKeWGkdQdcekpxQARAQAB
      tB9MYXVuY2hwYWQgUFBBIGZvciBPbmTFmWVqIFN1csO9iQJOBBMBCgA4FiEEuNx+
      U5RmVu+85MHdcdrqq0rUyrYFAmYo0vECGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC
      F4AACgkQcdrqq0rUyrYOPQ/+IArA4s1J3op/w7cXek0ieFHWHFDrxPYS+78/LF/J
      LoYZw0nIU5Ovr+LzehFMIQU6esgPXwbeCVgwLwat57augAkAYWT0UzH5dE6RKAGr
      C2vsHWVfPhQn6UndfzwXc0mTLGQni25aQaZ6k60Dbm/vblejrTQrtAUWoMO3Z1cr
      NDGJ3Z9DCxtr2o9gRYUI6HwLHJtobTIeI5xsr5x+GvXiIAVCPa3ZEuRL6jMQfqfS
      C43mpuiS1kGgsnQLs2DbN7EFCfiJoNX1QzZu25zg+IS9PXbCJnheZWnH0rwUSb/N
      hZPcSefGlNlhr824OfT30v79hQnw59XbsfV270O9jPbD4kttN+OiszbU66zsuiOh
      BO46XCckQPqDkBMw56GPFuVrQgGb1thXvn67URJgPyJhwauBWKPNAJ9Ojuo+yVq/
      hdR1VNWThXQbZgaGSWrbjt6FdYtQb9VX88uu5gFDmr180HogHNUDUcqNLLdnjfFs
      4DyJlusQ5I/a7cQ7nlkNgxAmHszwO/mGLBuGljDUYkwZDW9nqP1Q5Q2jMtrhgXvR
      2SOtufvecUbB7+eoRSaOnu7CNMATG6LocFEMzhKUde1uZTfWSqnYEcdqoFJMi46y
      qaNxhiNLsQ5OBMbgSp2zCbQxRBdITMVvBR5YjCetUIGEs6T1yQ5wh5Xpoi34ShHn
      v38=
      =kFlZ
      -----END PGP PUBLIC KEY BLOCK-----

In following up on the previous comment, I'm thinking about ways to automate retrieval of a key from the ubuntu keyserver and successive export in ASCII format. Here's a command shell one liner that I've come up with to automate the retrieval of the key in ASCII format on stdOut:

export TMPGNUPG=$(mktemp -d) && GNUPGHOME=$TMPGNUPG gpg -q --keyserver keyserver.ubuntu.com --recv-keys B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6 && GNUPGHOME=$TMPGNUPG gpg -ao - --export B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6 && rm -R $TMPGNUPG && unset TMPGNUPG

This could be run and registered in an ansible task to retrieve the ASCII string from stdOut.

Perhaps a simpler solution is to use the URL https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x to retrieve the key, and that could probably be used with get_url to save the key in ASCII format. This would then require a further ansible.builtin.file lookup to read the contents of the .asc file into a variable.

In any case you would still need to have the key fingerprint ahead of time. I suppose one way of retrieving the key fingerprint would be something along the lines of:

wget -qO - https://launchpad.net/~ondrej/+archive/ubuntu/php/ | grep LP.cache | sed -n 's/.*LP\.cache = \(.*\);<\/script>/\1/p' | jq -r '.context.signin
g_key_fingerprint'

As long as launchpad.net doesn't decide to change or remove the LP.cache object from archive pages, we can translate this into an automated ansible task:

- name: Extract signing key fingerprint from Launchpad PPA page
  hosts: localhost
  tasks:
    - name: Download PPA page content
      ansible.builtin.set_fact:
        ppa_page_content: "{{ lookup('ansible.builtin.url', 'https://launchpad.net/~ondrej/+archive/ubuntu/php/') }}"

    - name: Extract JSON from the PPA page content using regex
      ansible.builtin.set_fact:
        ppa_json_cache: "{{ ppa_page_content | regex_search('LP\\.cache = (\\{.*\\});</script>', '\\1') }}"

    - name: Parse JSON to extract signing key fingerprint
      ansible.builtin.set_fact:
        signing_key_fingerprint: "{{ ppa_json_cache | from_json | json_query('context.signing_key_fingerprint') }}"

    - name: Display the signing key fingerprint
      ansible.builtin.debug:
        msg: "The signing key fingerprint is: {{ signing_key_fingerprint }}"

I just found out that launchpad actually has an API (https://api.launchpad.net/devel.html) that makes retrieval of the public signing key fingerprint much easier without having to scrape the archive webpage. Here's an example that substitutes the scraping step above with a call to the API:

---
- name: Make API request to Launchpad
  uri:
    url: "https://api.launchpad.net/devel/~ondrej/+archive/ubuntu/php"
    return_content: yes
    method: GET
    headers:
      Accept: "application/json"
  register: launchpad_response

- name: Parse JSON to extract signing key fingerprint
  ansible.builtin.set_fact:
    signing_key_fingerprint: "{{ launchpad_response.json.signing_key_fingerprint }}"

Trying to use the jenkins example in an ubuntu22.04.5 system produced the following error:
E:Malformed entry 1 in sources file /etc/apt/sources.list.d/jenkins.sources (Suite), E:The list of sources could not be read.

X-Repolib-Name: jenkins
Types: deb
URIs: https://pkg.jenkins.io/debian-stable
Components: binary
Signed-By: /etc/apt/keyrings/jenkins.asc
Enabled: yes

it was missing suites: [binary]
should look like:

- name: Add Jenkins repo using key from URL.
ansible.builtin.deb822_repository:
name: jenkins
types: [deb]
uris: "https://pkg.jenkins.io/debian-stable"
components: [binary]
suites: [binary]
signed_by: https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
state: present
enabled: true